VAF v0.1.0a15
Pre-releaseAdded
-
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 withmulti(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, modelveyllo-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. Theveyllo-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()plusAgent(session=<id>)persist and resume a
conversation across process restarts (idempotent updates, loud failure on
unknown ids, tenant-ownership check underuser_scope; runnable chatbot
example included);vaf.markersexports 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
growsduration_msand a dispatch-levelokflag ontool_endplus new
llm_start/llm_endevents with token usage on API providers (attached
across backend swaps; facade shortcutagent.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
Agentcan now be handed extra tools directly:agent.add_tool(MyTool())
before the first run registers aBaseToolfor 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
thatfrom vaf import Agentis 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, theBaseTooldeclarative defaults, and the
pip entry-point tool discovery (vaf.toolsgroup). -
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.
- it had skimmed past the bare success banner. Both completion messages
- 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.
- and the nested schema requirement rejected the entire call with
- 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 thatrun_tempneeds 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
standardTool Error:/Security Error:/[PLAN REQUIRED]prefixes,
so a failedwrite_fileshowed 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 filesystemAccess denied:/Invalid pathrefusals, 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 successfulread_fileof 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 calledwrite_filewithfile_path/messageinstead of
path/content; the call failed schema validation and the write was
lost. Tools can now declare aninput_aliasesmap (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_filemaps 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_workflowlane: 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 promptworks in local mode now. Both scripting lanes
(vaf promptand thevaf run promptalias) 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 theveylloprovider; 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-survivingcustom_tools/,
pip entry points); the docs index describes server mode correctly and
lists three previously missing pages; the config reference gains rows
foranthropic_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 thespeechextra. 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).