Skip to content

VAF v0.1.0a15

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 18 Jul 02:19
· 337 commits to main since this release

Added

  • Voice call, the reflex features speak your language: the agent's "did you mean me?"
    clarification and the cue words behind it now ship in about 35 languages (generated
    from English, so unlisted languages fall back to English), and it detects being spoken
    to across the major languages, not just German and English. What it hears (speech
    recognition) and what it says (the spoken reply) were already multilingual; this widens
    the small fixed phrases in between. The proactive chime-in was also tuned so it actually
    speaks up on genuinely on-topic overheard talk instead of staying silent.

  • Voice call, you can interrupt the agent: while the agent is speaking you can now
    just start talking and it stops and listens instead of making you wait for it to
    finish. The microphone is opened with echo cancellation so it
    does not hear its own voice, and it only yields to a real, sustained interruption (a
    brief noise will not cut it off). For now this covers interrupting while it is
    speaking, on a web call; interrupting while it is still thinking, and having it resume
    where it left off, are planned next.

  • Voice call, the agent chimes in on interesting talk: during a live call the agent
    now keeps a short rolling transcript of what it hears, and when someone else in the
    room says something that matches your configured interest topics, it can briefly and
    naturally chime in with a grounded remark instead of staying silent. It never invents
    a reason to speak (a chime-in must be grounded in your topics, and the agent may still stay
    silent), never chimes in while it is busy with a task, and does not repeat itself. One
    simple dial, voice_awareness_activity (quiet..active), sets how readily it joins in
    (at the lowest setting it only listens); it behaves calmly on its own when you are in a
    conversation with someone else and more readily one-to-one, without you managing any
    modes. It also asks "did you mean me?" when an unclear speaker says something like "can
    you hear me?" and it cannot tell whether it was addressed. Tool actions stay yours
    alone (a guest still cannot make it do anything), and your private context is never
    used to chime in for a guest.

  • Per-speaker language hint for cloud STT: the shared speech client now caches
    the language the cloud provider already returns and passes it as a hint on the
    next transcription (a more precise, cheaper call), instead of running a local
    model to pre-detect it. The cache is keyed per speaker (the web mic uses the
    user's scope, so it stays user-isolated), and to catch a mid-conversation
    language switch it re-detects hint-free every few turns and always refreshes from
    the actually-detected language. No new dependency and no pre-call overhead. The
    hint is language-agnostic (normalized to ISO-639-1, ISO-639-3 mapped, locales
    reduced to base) so it works across every supported language, and the Veyllo lane
    auto-detects with multi (automatic code-switching) when no language is pinned.

  • Veyllo speech-to-text: Veyllo is now selectable as a cloud STT provider
    (speech_stt_provider = veyllo, model veyllo-transcribe), using the same
    API key and base URL as the Veyllo chat/vision provider. The first time a Veyllo
    key is added (at onboarding or later in Settings) while no STT provider was
    chosen, Veyllo becomes the default STT lane, and it always falls back to the
    local engine on any failure (no internet, empty credits, timeout); an explicit
    later choice (local, OpenAI, ElevenLabs) overrides it. The veyllo-transcribe
    audio model is filtered out of the chat-model dropdowns.

  • Data Explorer legibility: deleting a chat now saves the chat's title
    into the surviving workspace folder, so folders left behind by deleted
    chats keep their human name instead of showing a raw internal folder name
    (an explicit rename always wins; folders orphaned before this change keep
    their old names). The badge on such folders now reads "chat deleted"
    instead of the jargon "orphan", the list sorts live chats first and
    deleted-chat folders to the end, and a new info button in the header
    explains the folder colors, the badge, and the current-chat dot. The
    window now keeps a fixed height with a refined scrollbar instead of
    growing and shrinking with the workspace count, and each tile counts
    its folders too (a workspace holding only a subfolder no longer reads
    "0 files" as if it were empty).

  • Data Explorer search: a search field in the "My Workspaces" header
    filters workspaces by name instantly and also searches file names and
    text-file contents inside them (server-side, scoped to the user's own
    workspaces, bounded per workspace); matching files are shown under each
    workspace tile with a content snippet in the tooltip.

  • Three new developer docs make the existing machinery usable from
    outside
    , each verified against the code: an observability guide
    (structured tool/gate events via the engine's event sink, plus the
    machine-readable NDJSON mode of the scripting CLI for integrating VAF as a
    subprocess from any language), a debugging guide (the complete map of log
    files, how to redirect them, what the debug switch does and does not
    silence, how to read a session file), and an engine reference (the
    constructor, lifecycle, turn and tool-dispatch contracts, and concurrency
    rules of the advanced embedding class). The embedding guide gains a
    security-posture section (what needs Docker, what runs on the host, what
    ports are and are not opened), pointers to the other extension points
    (workflows, skills, MCP servers, the update-surviving custom-tools folder),
    and an honest note on custom OpenAI-compatible endpoints. Key engine
    methods now carry docstrings.

  • Persistent conversations, richer events, async and marker constants on
    the library facade.
    Four additions for developers embedding VAF:
    agent.save_session() plus Agent(session=<id>) persist and resume a
    conversation across process restarts (idempotent updates, loud failure on
    unknown ids, tenant-ownership check under user_scope; runnable chatbot
    example included); vaf.markers exports the special return-value strings
    (SYSTEM_LOG_ONLY, GENERATION_STOPPED, ...) as constants with a CI
    guard pinning them against the engine source; the structured event sink
    grows duration_ms and a dispatch-level ok flag on tool_end plus new
    llm_start/llm_end events with token usage on API providers (attached
    across backend swaps; facade shortcut agent.on_event(cb)); and
    await agent.run_async(...) runs a turn in a worker thread for
    event-loop applications - documented honestly as a thread-executor
    wrapper, not native async.

  • Multi-tenant embedding: Agent(user_scope=...). An application
    embedding VAF can now assert which end user a conversation belongs to
    with one parameter. The value is validated as a UUID at construction
    (a bad scope fails loudly instead of silently operating on the machine
    owner's data), the account username is resolved together with the scope
    (never the admin fallback), the identity is bound before the system
    prompt is built and re-asserted on every turn, and memory/reminders/
    per-user files then key on that scope with the product's fail-closed
    filters. The embedding guide gains a "Multi-tenant embedding" section
    spelling out the trust model and the hard limits (one tenant per
    process, the machine-global trust store, shared on-disk config, no
    reliance on database-level isolation yet), and now states honestly that
    a bare unscoped agent acts as the machine owner in local mode. Also
    fixed: the sandbox tool now receives the user scope from the dispatcher
    (spoof-proof direct assignment), so its per-user container work
    directories key on the calling user (previously every main-agent run
    shared one prefix).

  • One provider registry instead of eleven copies. The LLM provider set
    and its endpoints now live in a single source of truth
    (vaf/core/provider_registry.py); the backend factory, the coder's
    endpoint map, live model discovery (both copies), the vision-capability
    check (previously three manually-synced copies that had drifted apart) and
    the CLI settings menus all read from it, guarded by new CI sync tests plus
    a factory-pinning test that locks today's behavior byte for byte. Four
    real drift bugs died in the process: the CLI provider menus did not offer
    Veyllo at all, the provider-coverage test itself skipped Veyllo, and two
    of the three vision checks did not recognize Veyllo models as
    vision-capable. Provider-specific behavior (DeepSeek reasoning fields,
    OpenAI reasoning-parameter gating, Veyllo tool-call-id handling) stays in
    gated code paths, unchanged.

  • Per-instance tool registration on the library facade. An embedded
    Agent can now be handed extra tools directly: agent.add_tool(MyTool())
    before the first run registers a BaseTool for that instance only - no
    pip package, no file drop-in. Late or invalid registrations raise clear
    errors, and the facade CI guard pins the behavior.

  • A developer FAQ. Short verified answers to the questions developers
    actually hit: Docker requirements, the confirmation-gate error string,
    thread-safety, the engine's return contract, log redirection, custom
    endpoints, the tool lanes, and what the alpha stability promise covers.

  • A runnable examples/ directory. Five self-contained artifacts for the
    embedding surface: the five-line quickstart, streaming plus the structured
    event sink, driving VAF as a subprocess via the NDJSON output format (the
    pattern for non-Python integrations), per-instance tool registration, and
    a complete pip-installable custom-tool package using the entry-point
    mechanism. A CI test keeps every
    example compiling and the example tool loadable, and the license-header
    check now covers the examples tree.

  • A CI guard now protects the public library surface. The docs promise
    that from vaf import Agent is safe to build on; until now no test
    imported that facade, so a breaking change to it could have shipped with
    a green CI. A new executable contract pins the facade exports, the
    Agent/run() signatures, the BaseTool declarative defaults, and the
    pip entry-point tool discovery (vaf.tools group).

  • Setting up a voice profile no longer stalls on slow connections: the
    speaker-engine models (26 MB) now download in the background during the
    enrollment intro instead of blocking the first round.

Changed

  • Voice call, guest privacy: on a live call with an enrolled voice profile, a guest
    (a speaker the voice check does not verify as you) who talks to the agent now gets a
    reply built WITHOUT your private context - your chat digest, your memory, and the
    prior call history are all withheld from that turn entirely, not just guarded by a
    prompt rule, and the agent is told to help only with general questions and never share
    your memory, notes, schedule, messages or contacts. So a guest cannot make the agent
    replay your earlier turns by asking "what did you just say?". Tool actions already
    stayed yours alone; this closes the matching information side.

Fixed

  • A finished workflow now tells the model, imperatively, not to redo the
    work.
    A verified successful run (finished HTML on disk) was followed
    by the model rebuilding every step manually and reporting total failure
    • it had skimmed past the bare success banner. Both completion messages
      now lead with an explicit directive (work is done; do not redo steps,
      re-run searches or rebuild files; present the results and file path)
      before the output and the per-step summary. The docs also gained an
      honest "small local models - known behavioral limits" section
      (PROVIDER_MODES.md): redo-after-success, narrated intentions and
      round-trip fragility are model limitations the framework mitigates but
      cannot remove; assigning a stronger sub-agent/coder model
      (subagent_model) is the user-facing lever.
  • A workflow step's instruction survives the presence of extra
    parameters.
    The retest's model authored a step exactly the way the
    tool schema teaches - instruction in input, extra parameters in args
    ({"max_results": 3}) - and the engine built the tool call ONLY from
    args, silently dropping the input: web_search ran without a query and
    the whole run failed with "Error: No query provided." The resolved
    input now fills the tool's missing primary parameter; steps whose args
    already carry it (every saved template) are untouched.
  • A workflow's completion message shows the real work of every step, and
    three templates no longer end on a filesystem agent asked to write
    prose.
    A retest run succeeded end to end (11-minute coder step, HTML
    written) - but the template's final step asked the librarian agent, a
    FILESYSTEM agent, to "write a short completion message ... where the
    file was saved". It parsed that as a file search and returned "No files
    found matching 'was'", which became the workflow's final output. The
    model read that next to "completed successfully", concluded the run
    produced nothing, and redid every step manually - 42 steps, three
    duplicate deliverables. The librarian completion step is removed from
    research_and_code, generate_docs and create_file (the save step's own
    "File written successfully to " is the honest completion), and
    both completion messages (saved workflows and temporary ones) now
    append a bounded per-step result summary, so one weird step can never
    hide the actual deliverables again.
  • The validation question no longer costs the workflow run - the system
    answers it itself.
    When a temporary workflow's content steps carried
    no validate flags, run_temp bounced with "[VALIDATION CHECK] ... call
    run_temp again with the flags or skip_validation". A live retest showed
    a weak model bouncing off that twice (retrying without the flags both
    times) and then doing every step manually while its correctly authored
    workflow never ran. Validation on deliverable steps is exactly what the
    bounce text recommended - so run_temp now enables it on those steps
    automatically and runs; skip_validation stays the explicit opt-out.
  • A temporary workflow with mangled step field names is repaired instead
    of rejected.
    The retest's model got everything right except the step
    FIELD NAMES: tool in "action", instruction in "description", no "input"
    • and the nested schema requirement rejected the entire call with
      "'input' is a required property", a message the model could not act on;
      it regressed into planning spin until the loop guards ended the turn
      with nothing produced. Steps are now repaired before validation (tool
      also accepted from action/agent_id; input falls back through
      task/prompt/instruction/query/description/name; args-only steps get a
      synthesized label), the hard nested requirement is gone, and a step
      with truly nothing usable gets a targeted error instead of a schema
      dump. Applies to temporary and saved workflow authoring alike.
  • A model can no longer talk itself into believing its own fictional
    diary.
    Two consecutive retest chats failed the same way: the model
    narrated its INTENTIONS into working-memory notes ("Web-Suche: läuft",
    "Workflow wurde erfolgreich gestartet") without calling one real tool;
    the working-memory block re-injected that fiction as trusted context,
    the anti-spin guard then forced a text-only turn whose wording ("state
    your result") invited a result, and the model coherently reported a
    finished workflow with an invented file path. The result-grounding
    judge (the same small local model) waved it through. Four guards now
    close the loop end to end: (1) a note firewall refuses outcome- or
    progress-claiming notes while no real tool has run this turn (facts,
    not intentions - the note is re-fed as truth later); (2) result
    grounding gained a deterministic rule that needs no LLM judge: a final
    reply asserting tool outcomes after a bookkeeping-only turn is
    ungrounded by construction (purely conversational recap turns still go
    to the judge); (3) the anti-spin escalation now explicitly forbids
    claiming results and demands an honest still-open answer; (4)
    working-memory timestamps are rendered in the user's timezone in the
    prompt (they were UTC, so the model misdated its own recent actions by
    hours).
  • A healthy coder inside a workflow is no longer executed at minute
    five.
    The generic sub-agent hard cap (300s) applied to workflow steps
    too, and killed a perfectly healthy coding-agent step mid-loop - linter
    green, actively streaming - with SIGTERM, failing the whole workflow at
    its last step (live incident). Heavy agent steps (coder, research,
    document) inside a workflow now get a worst-case floor of 30 minutes
    (workflow_agent_step_timeout_seconds); this is safe because a dead or
    stuck child is caught much earlier by the heartbeat liveness guard
    (~60s) - the hard cap is only the absolute-runaway backstop, and it was
    doing the killing instead. All other tools keep their normal budgets.
  • The SubAgent window no longer opens on top of a running workflow.
    The intended design routes an embedded sub-agent step's output into the
    workflow panel's terminal, and the window-open helper honors that - but
    the sub-agent heartbeat handler set the window open directly, bypassing
    the guard, so the coder step opened a duplicate window next to the
    runtime panel. The heartbeat path now carries the same workflow guard;
    a manual open by the user still works.
  • The plan gate no longer bounces a workflow launch - the launch IS the
    plan.
    The retest showed the exact cost of that bounce: the model
    committed to execute_workflow with the suggested template, got
    [PLAN REQUIRED], dutifully set a plan - and then did the steps manually,
    the workflow forgotten. A saved template id or a run_temp steps list is
    precisely the approach the gate exists to demand, so the gate now seeds
    working memory's plan from the launch call itself (observability
    preserved, placeholder detection unaffected) and lets it through.
    Launch calls without a plan payload (list/delete actions) still bounce,
    and every other state-changing tool is gated unchanged.
  • The duplicate-call guards now cover text-recovered tool calls too.
    The in-batch dedupe and the windowed redundant-read check lived inside
    the streamed-call loop, but a weak local model's calls often arrive via
    the text-recovery fallback parsers, which bypassed both - the retest
    ran the same two searches twice within one second out of a
    fallback-parsed batch. Both checks moved into one cross-lane filter
    that runs after every parsing lane and before the calls are committed
    to history, so streamed and recovered calls get identical protection.
  • A temporary workflow's final build step now actually receives the data
    the earlier steps gathered.
    The engine passes step results only through
    explicit placeholders in the next step's template - and a weak model
    reliably NAMES its outputs but never references them: the retest's
    workflow ran both web searches perfectly and then told the coder "use
    the results from the previous searches" in prose, with no placeholder
    anywhere. The coder received zero data and, correctly following the
    strict factual-data policy, rendered [DATA NOT FOUND] into every field
    of an otherwise finished page. When a task-consuming agent step (the
    coding agent, document writer, document agent, librarian and browser
    agents - builders and analyzers alike) references no prior step output,
    the engine now auto-attaches a bounded digest of the actual results to
    that step's instruction, in every lane the engine runs (temporary
    workflows, saved templates, the CLI lane, automations). The research
    agent is deliberately excluded (its input is a short topic query, and
    its job is producing data, not consuming it); templates that DO
    reference outputs - every saved template - are never touched. The
    authored step list of a temporary workflow is now also logged in full,
    since during the forensic it existed nowhere.
  • A pending background question can no longer hijack the user's next
    request.
    The proactive thinking pass asks questions over the user's
    messenger and latches "waiting for a reply" per user. When the user
    then typed a brand-new task into a fresh chat, the pickup classified
    it as THE reply: the model got told "the user answered your background
    question", blended both topics (they happened to be similar), replied
    over the messenger unprompted, and the reply-confirmation gate then
    blocked the user's own workflow-builder calls twice - the turn ended
    after 27 confused steps with a stale answer, although the task had
    been completed midway. Three fixes: (1) the pickup lane now reads the
    RAW message - the WebUI workspace preamble had defeated the leading
    -text confirmation check (proven: raw "Okay fuehre bitte..." passes,
    enriched fails) and was even stored as the recorded reply; (2) the
    reply is classified three-way - short yes carries the proposal, short
    no declines it, and a LONG task-shaped message counts as a NEW topic:
    light context note only, no "carry out the proposal" framing, and the
    mutation gate stays disarmed so the user's own request is never
    confirmation-blocked; (3) a TTL safety net (default 12h) expires a
    stale waiting latch at read time, since the regular 10-minute skip
    only runs while thinking runs actually fire.
  • A model that builds the right workflow in the wrong wrapper now gets
    its own call handed back to copy.
    Follow-up to the routing fix below,
    from the retest: the model merged the two workflow hints into
    execute_workflow(workflow_id="create_agent_workflow", variables={action,
    steps}) - a complete, CORRECT temporary-workflow payload inside the
    wrong tool. The prose-only redirect explained the mistake, and the
    model gave up on workflows and did every step manually. When the
    mistaken call carries usable steps, the redirect now echoes the exact
    create_agent_workflow(...) call to copy, arguments verbatim - weak
    models copy reliably but rephrase poorly. Still a message, never an
    auto-forward.
  • Verbatim re-runs of a lookup that already succeeded this turn are
    refused, and an identical duplicate within one response is dropped.

    The redundant-call guard compared a new tool call only against the
    newest tool message, so a single interleaved call (a failed workflow
    attempt, a plan-gate bounce) hid an exact repeat from it - the same
    retest burned four calls re-running two web searches word for word,
    including the same search accepted twice in one second (for a send
    tool that same hole would have meant a double-send). Pure lookups
    (searches, reads, listings) whose identical call already succeeded in
    the current turn are now refused with a pointer to the existing
    result, EXCEPT when a mutating tool succeeded in between (a re-read
    after a write is legitimate; the guard fails open). Exact duplicates
    within one model response are dropped silently for all tools - one
    logical call, one result.
  • The workflow router routes on what the user actually said. The WebUI
    lane prepends a workspace context preamble to the user message before the
    agent runs, and the workflow router matched templates against that
    combined text: the preamble's wording (coding_agent, projects,
    write_file) steered a plain websearch-and-HTML request to a CODE
    workflow, and the variable extractor stuffed the entire preamble into
    the workflow's query variable (live incident: a 44-step turn in which
    the model rightly declined the garbage suggestion and then did every
    step manually). Routing decisions - the template match, variable
    extraction, the explicit workflow parse, workflow-mention detection,
    language detection and the intent lock - now run on the raw,
    pre-enrichment message; the LLM still sees the enriched text.
  • A wrong template match can no longer eat an explicit workflow
    request.
    The run_temp advisory used to exist only in the no-match
    branch, so when the router matched ANY template (even a bad one) for a
    message that explicitly asked for a workflow, the suggestion block was
    the only workflow path shown. When the user's own message mentions a
    workflow, the suggestion now also offers
    create_agent_workflow(action='run_temp') as the fallback - advisory,
    with the same typo-tolerant detection both hints share.
  • Two more weak-model argument names are repaired instead of rejected.
    The same incident burned eight calls on schema rejections: write_file
    with file_content instead of content (four times) and python_exec with
    task instead of code (twice). Both observed names (plus contents and
    script) are now in the tools' alias maps, so the call is repaired and
    dispatched on the first attempt.
  • A tool call is no longer silently dropped when a model garbles its own
    closing tags.
    The recovery parser for text-written tool calls (a
    reasoning model sometimes emits <tool_call><function=NAME>... as plain
    text instead of a native call) required an exact
    </function></tool_call> close to accept anything. A live case showed a
    local model trail off into hallucinated, unrelated closing tags instead
    of its own; the properly-closed parameter inside was perfectly
    recoverable, but the strict match returned nothing and the turn ended
    with tool-call-shaped text visible in history and no tool ever actually
    invoked. Three earlier drafts of this fix failed adversarial review
    with real false-positive execution risk (one could dispatch a
    destructive call out of a model's PROSE about the syntax if ordinary
    HTML appeared later in the reply; one silently dropped a legitimate
    parameter whose value merely mentioned closing-tag-shaped text; one
    accepted a single wrong-named closing tag, which an example wrapped in
    a code block satisfies by construction). The shipped design parses
    parameters one at a time, each bounded only by its own </parameter>;
    a strict close is accepted anywhere, while lenient recovery requires
    the call to start at a line start AND either two-plus consecutive
    wrong-named closing tags sitting immediately (whitespace only) after
    the last parameter, or the next <tool_call> beginning right there
    (back-to-back calls with the first close forgotten - recovering those
    was itself a review finding, they previously vanished silently). A
    well-formed call parses exactly as before, the incident's malformed
    close is recovered, and inline mentions in prose or markup are
    rejected.
  • A request to "build a workflow for this" no longer gets talked out of
    it.
    When the router finds no SAVED workflow template for a request
    (most requests - templates are a fixed catalog, not every task fits
    one), the fallback hint told the model "most simple requests (weather,
    news, questions) don't need workflows" and pointed only at
    list_workflows (saved templates only) - never at the ad-hoc builder.
    A user who explicitly asked to run a weather lookup as a temporary
    workflow got a hint that contradicted their own request almost word
    for word, and the model complied with the hint over the user (live
    incident). The no-match hint now detects a workflow mention in the
    user's own message (typo-tolerant - the real request had "workflow"
    mistyped as "workflwo") and surfaces
    create_agent_workflow(action="run_temp") as the option to use
    instead; the detection is a cheap substring match that can also fire
    on an unrelated mention ("workforce", "my daily workflow" as small
    talk), so the wording stays advisory either way - it defers to the
    model's own judgment rather than pushing a directive - and now warns
    in both branches that run_temp needs 2+ chained steps, matching the
    tool's own single-step rejection.
  • Every chat now shows its workspace-folder chip immediately, and an
    unused one no longer lingers forever.
    The chip is "this chat has its
    own workspace" - a standing affordance, not a "you already saved
    something" indicator - so opening a chat now creates its (empty)
    workspace folder right away instead of only after a tool happened to
    write into it. To avoid littering the projects folder with abandoned
    empty directories, deleting a chat now also removes its workspace
    folder when it is still empty at that point (a folder holding real
    content is left untouched either way).
  • The delete-time workspace cleanup got two safety rails (audit
    findings on the entry above). While a sub-agent or workflow is still
    running for the chat, deleting the chat skips workspace removal
    entirely: the run may drop its first output file between the "is it
    empty" check and the removal, and files must never be deleted out from
    under a live run. And the recursive emptiness check now surfaces
    unreadable subdirectories instead of silently skipping them (the
    underlying directory walk ignores permission errors by default), so a
    permission-denied subtree full of files can no longer classify the
    whole workspace as "empty" and get it deleted - anything that cannot be
    fully inspected is kept.
  • A session without a recorded owner is no longer treated as owned by
    everyone in the workspace endpoints.
    The HTTP workspace lane (browse,
    upload, file delete, plus the image-describe session cache) accepted
    any authenticated user as the owner of a legacy session that predates
    user scoping, while the WebSocket gate treats exactly those sessions as
    admin-only - so on a multi-user server, a non-admin user could browse,
    upload into, create, or delete files inside such a session's workspace.
    Both HTTP checks now enforce the same rule as the WebSocket gate:
    scopeless sessions are admin-only, admin detected role-aware. Scopeless
    sessions were not purely theoretical: an automation delivering to a
    messenger contact BEFORE that user ever wrote inbound created the
    channel session without an owner scope (the inbound lane always stamped
    it; the two outbound-first creators did not, despite having the scope
    in hand) - those sessions are now stamped with their owner at creation,
    so their real owner keeps normal access on multi-user servers instead
    of falling into the admin-only legacy bucket. Pre-existing scopeless
    session files on disk stay admin-only in both lanes, exactly as the
    WebSocket gate already treated them.
  • A local model calling the wrong "workflow" tool now gets redirected,
    not just a template list.
    execute_workflow(workflow_id=...) takes a
    saved template id; a weak model tried
    workflow_id="create_agent_workflow" - the NAME of the other workflow
    tool, not a template - and got a plain "not found" listing that didn't
    explain the actual mistake. Both tools' descriptions are now explicit
    about the distinction, and the error message detects a live tool-name
    collision and points at the right tool to call instead.
  • A failed tool no longer reports itself to the model as "OK". The
    per-turn tool-context summary (what the model reads to know what its
    tools did) labeled failures by an out-of-date heuristic that missed the
    standard Tool Error: / Security Error: / [PLAN REQUIRED] prefixes,
    so a failed write_file showed as -> OK: Tool Error ...; a local model
    then told the user the file existed when it did not (live incident). The
    three copies of the "is this a failed tool result" check (retry guard,
    per-turn summary) now share one prefix-anchored helper so they cannot
    drift again. A follow-up adversarial review of that fix then found the
    shared detector covered only a handful of the failure shapes shipping
    tools actually return; a repo-wide sweep added roughly thirty more
    currently-live families (the [BLOCKED]/[HOST]/[SECURITY] gate
    markers, the filesystem Access denied:/Invalid path refusals, the
    messaging "unavailable:" family, "X failed:" shapes, MCP/HTTP error
    idioms, connection preconditions, unimplemented-stub replies, and the
    banner-prefixed python runner marker), each pinned by a test built from
    the tool's real return string. A third adversarial pass then caught the
    first version of THAT expansion over-rotating: it scanned whole results
    with free substrings, so a successful read_file of a log mentioning
    "connection failed:" rendered as a failure (10/10 realistic
    content-carrying reads misclassified). The shipped detector therefore
    anchors every family that starts the message, bounds banner-tolerant
    markers to the first 200 characters, gates stub-reply phrases on the
    result being short, and pins the content-carrying success class with
    its own tests. The three thinking-mode soft-block nudges now lead with
    [BLOCKED] so a blocked gather call also reads as blocked instead of
    green.
  • Common weak-model argument-name mistakes are now repaired. A local
    model called write_file with file_path/message instead of
    path/content; the call failed schema validation and the write was
    lost. Tools can now declare an input_aliases map (kept off the model-facing
    schema so no provider can reject it), and the input-repair layer remaps a
    present alias to the canonical name before dispatch (conservatively:
    never overwriting a supplied canonical key, never guessing between two
    aliases). write_file maps the obvious path/content synonyms.
  • Stop now actually stops a running web search. Stopping a turn
    abandons the tool's worker thread (Python cannot kill threads), and the
    abandoned search kept crawling pages and calling the local model for
    summaries long after the stop - occupying the single llama server with
    dead work. The runner now hands every bounded worker a cancellation
    signal that, unlike the shared stop flag, cannot be cleared out from
    under it; the web search checks it before every page read, every
    summary call and the final synthesis, and exits early. Bonus fix found
    in the same logs: those summary calls burned their whole token budget
    on reasoning and returned empty text on thinking-capable local models -
    tool-side utility completions now disable thinking, like the voice lane
    already did.
  • A workflow cannot be started twice for the same chat anymore. After
    an empty-response recovery reset, a local model could forget it had
    already delegated and call the same workflow again while the first run
    was still live (observed: two concurrent research workflows sharing one
    GPU). Both launch lanes now check the session's live tasks and refuse a
    duplicate with an honest still-in-progress status, mirroring the
    sub-agent re-delegation guard. An adversarial re-audit then found the
    first version of that guard structurally broken for the in-process
    execute_workflow lane: it read the task registry but the tool never
    registered its own runs there, so two concurrent calls both sailed past
    it (verified with a live repro). The tool now registers itself for the
    duration of the run (deregistered on every exit path), both lanes share
    one guard predicate, and that predicate also counts a freshly-created
    task whose spawned terminal has not reached "running" yet, closing the
    spawn-window race in the terminal lane. A second adversarial pass on
    the fix itself then forced two more rounds of hardening: the
    registration now heartbeats (without one, the zombie reaper failed it
    90 seconds into exactly the multi-minute runs the guard protects,
    silently reopening the guard and queueing a spurious crash report), and
    both lanes verify AFTER registering who won the registry slot, since
    the pre-check alone still let two barrier-synced concurrent calls both
    through. The tool's cleanup also consumes any result another actor
    (stop-all, a reaper) queued for its task id while it ran, so a
    synchronous run can never additionally surface as a phantom sub-agent
    delivery. A failed terminal spawn now deregisters its task and falls
    back to inline execution instead of reporting an async run that never
    started.
  • Sub-agent task registry updates can no longer erase each other. The
    IPC task queues (pending/active/results) are JSON files mutated by
    read-modify-write sequences whose per-file locks only covered a single
    read or write - two concurrent mutations (for example two tasks being
    marked running at once) could interleave and silently drop one side's
    update, observed live as two workflow launches erasing each other's
    registry entry and both slipping past the duplicate guard. All registry
    mutations now serialize through one reentrant in-process lock plus a
    bounded cross-process file lock held for the whole read-modify-write,
    with a best-effort fallback (a stuck lock degrades to the old behavior
    after five seconds rather than wedging a chat turn).
  • The workflow terminal stream no longer freezes the app window. When a
    workflow step drew a live progress animation (the research agent), every
    animation frame line was forwarded to the Web UI as its own event -
    hundreds per second, full of raw ANSI color codes - until the page froze
    and the WebSocket dropped (live incident). The mirror now enforces ticker
    semantics at the emit site: ANSI escapes stripped, empty animation frames
    and duplicate redraws dropped, and a hard rate cap with an honest
    "[... N lines skipped]" marker. The real terminal still shows everything.
  • vaf prompt works in local mode now. Both scripting lanes
    (vaf prompt and the vaf run prompt alias) never loaded the local
    model, so every local-mode invocation returned an empty answer - the
    same bug class as the library-facade fix below, found by pre-push smoke
    testing and verified live against real hardware (model load, NDJSON
    stream, server reused by the running app afterwards).
  • Workflow steps run the sandbox under the calling user's scope. The
    workflow engine's own tool-argument injection (a deliberate narrower
    copy of the agent dispatcher) did not cover the sandbox tool; its
    per-user container work directories now key on the user in workflow
    runs too, and a CI guard pins the engine copy's coverage.
  • Embedding a local model actually works now. The documented library
    quickstart with the local provider silently returned an empty string: the
    facade never loaded the model, so the turn aborted before generation. The
    facade now downloads/starts (or reuses) the one local llama server on
    first use, exactly like the CLI does. Also corrected in the embedding
    guide: the real location of the trust store (the platform config dir, not
    the VAF home dir) and the fact that the "system" permission level bypasses
    the confirmation gate rather than triggering it.
  • Documentation corrections across the developer docs, each verified
    against the code: the embedding guide now names PySide6 (not PyQt6) in
    the desktop extra and includes the veyllo provider; the memory doc no
    longer claims "all memory content" is encrypted at rest (embedding
    vectors and titles/tags metadata are not - the encryption section
    spells out exactly what is and is not); the contributing guide's lint
    and format instructions now match what CI actually gates on instead of
    commands that would reformat 480 files; the architecture doc reconciles
    the "stable surface" promise with the alpha status and links the
    backward-compatibility rules; the Web UI flow doc's log-directory
    resolution order matches the code; the tools guide recommends native
    MCP server registration over the raw low-level tool, and its FAQ now
    covers all three tool lanes (in-tree, update-surviving custom_tools/,
    pip entry points); the docs index describes server mode correctly and
    lists three previously missing pages; the config reference gains rows
    for anthropic_prompt_cache, anthropic_thinking, and the
    ux_auto_open_* keys.
  • Voice-profile enrollment works on fresh installs. Setting up a voice
    profile answered "I could not hear speech" on every round of a clean
    install while the microphone was fine: the speaker-identification engine
    (sherpa-onnx) was never a declared dependency - it is in requirements
    now (hash-pinned lock refreshed) and in the speech extra. Two more
    layers of the same incident: the voice-activity model is now downloaded
    by the VAD path itself (the first enrollment round used to fail on a
    missing model file even with the engine installed), and an engine
    failure is spoken as its own message ("the voice-profile engine is not
    available - this is not about your voice") instead of sending the user
    into a speak-louder loop. The microphone WAV converter is also unified
    into one shared implementation that writes the recording's actual sample
    rate into the header (older WebKit builds ignore the requested 16 kHz
    and would have produced files whose header lied about the audio).