Releases: vstorm-co/agenticos
Releases · vstorm-co/agenticos
Release list
v0.0.343
Changed
- The org teardown's external cleanup is a durable Prefect deployment. It was
deferred to an in-processspawn_after_committask (#1137), which closed the
commit-ordering window but left a durability gap:spawn_after_commitis not
durable, so a process that died after the commit but before or during the cleanup
lost it - orphaning arag_<collection>table and its files with no record of what
to drop.OrganizationService.purgestill hands the work over after the commit, but
hands over arun_deploymentsubmission rather than the work: the run and its
parameters - the paths and collection names, all that is left of the deleted rows -
are recorded on the Prefect server, executed by a worker, and re-run by the flow's
retriesif that worker dies. Newapp/worker/tasks/teardown_tasks.pyholds the
idempotent cleanup, the durable flow wrapper and the submit-and-return dispatch; the
flow re-checks each collection name against the knowledge-base table before dropping
it, because the name is not tenant-unique (#913), so a name a second organization
claimed between the commit and the drop keeps its table. The inline
_purge_external_stateand_collection_still_referencedgo with it. (#1274) - The gap that remains is commit-to-dispatch: a crash before
spawn_after_commit
fires still loses the cleanup, which only a record committed with the delete - an
outbox - would close. (#1274)
v0.0.342
Fixed
LocalFileStorage.deletewasasync defover three blocking syscalls with no
yield point -realpathinside_resolve_safe_path,Path.existsand
Path.unlink. The RAG teardown loops (collection drop, knowledge-base delete) call it
once per file with no bound, so dropping a large collection unlinked every upload in a
single event-loop turn and stalled every other request the worker was serving. It
runs throughrun_blockingnow - the dedicated file poolloadalready uses - so
every caller yields and the teardown loops interleave. This is thedeletecase #25
did not reach: it offloadedsaveandload, anddeleteonly became a hot path
once the bulk teardown loops started calling it per file. Behaviour is unchanged; it
still removes the file and tolerates a missing one. (#1294)
v0.0.341
Fixed
- The card grids clipped by 34px on a 390px viewport.
grid gap-3 md:grid-cols-2 xl:grid-cols-3declares no column count belowmd, so
the single implicit track isautoand sizes to its items' content: the grid box
measured 324px while its owngrid-template-columnscomputed 391.094px.
min-width: 0injected at every level of the ancestor chain changes nothing - the
track is what is too wide, not the item - wheregrid-cols-1, that is
repeat(1, minmax(0, 1fr)), makes the track the container's 324px and the card's own
truncatefinally has something to truncate against. Applied to fourteen grids
across nine files: the ones whose cards carry user-supplied unbreakable text - a
slug, an email, a URL, an id - because those are the ones whose min-content is
unbounded. The sweep found 71 grids with the same shape and deliberately leaves the
other 57: the pattern is only a defect when something inside cannot be broken, and a
no-op class on 57 files is a diff nobody can review. (#120) - The chat control bar ran 27px past the composer at 390px. Three controls, 358px
of them, in ajustify-betweenrow with the connection pill. The pill isshrink-0
now - two words, nothing to give - and the control groupmin-w-0, which
AgentPicker's trigger needed too: itsmax-w-[160px]on the name is a cap, not
permission for a flex item to shrink. (#120) - A dashboard widget's info button was a 14x14 tap target, a third of the 44px both
mobile platforms ask for. Abefore:absolute before:-inset-[15px]pseudo-element
takes it to 44x44 without moving anything on screen. (#120)
Changed
- #120 had "describe it later" in every field, so the issue body is now the audit:
measured at 390x780 and 768x1024 in Chromium against the running app, fourteen pages,
each scrolled to the end, recording overflow, tap-target size, text size and anything
intersecting the fixed tab bar. Good news up front - the document never scrolls
horizontally, at either width, on any of the fourteen pages. Three things are
deliberately left and scoped on the issue rather than fixed here: the three data
tables, which each sit in their ownoverflow-x-autoscroller so no column is lost
but which want to be cards belowmd(Activity is 1155px in a 364px column); 45
sub-40px tap targets, most of them the repository's ownicon-sm, which want one
hit-area token belowmdrather than bigger buttons; and the 10-11px mono label
register, which is a design decision about a phone. (#120)
v0.0.340
Added
- A README front page: hero, pitch, badges, nav, then the spec sample and what the
product looks like. The content was already strong; what it lacked was the visual
first impression. The graphics are authored in this repository - no icon package, no
external asset host, no hand-copied path data:
.github/assets/hero-{light,dark}.svg, one per theme and served through a
<picture>, drawing the sentence the README opens with rather than decorating it -
one spec, one runner, the surfaces that reach it and the four refusals underneath,
in the app's own palette read out ofglobals.cssand converted rather than
eyeballed; anddocs/assets/mark.svg, the same mark alone, which is now the docs
site's logo and favicon, which the site did not have. One file, not two: an earlier
draft had a copy in.github/assets/, which is the second-source defect this
repository keeps citing. (#783) - Release and stars badges - the two the header lacked - plus a star-history image
before the footer. Image paths are relative, so they render in a pull request as
well as onmain; absoluteraw.githubusercontentURLs would show broken images to
whoever reviews a change about graphics. (#783) - Three screenshots - the builder, the catalog, the chat surface - at 1600px,
palette-reduced to 220 colours, 392KB for all three. Taken from amake devrun
against a throwaway database rather than a developer's own, which held eleven E2E
fixtures and four scratch agents; the fresh one was dropped afterwards and the
developer's eleven agents verified still there. No shot shows a live model answer,
deliberately: the alternatives were to spend somebody's tokens or to fake a
transcript, so the chat shot is the composer with a real question typed and the agent
picker showing which agent will answer. (#783)
Fixed
- The docs table promised "Spec, version, exposure, run - the four nouns" where
concepts.mdhas five: the trigger was added and one of the two pages updated.
(#783)
v0.0.339
Fixed
/runshad 0px under its last run row where every other list page gets 64px. It
was inFULL_HEIGHT_ROUTES, soPageTransitiongave itmin-h-0and withheld
PAGE_CLEARANCE- and it stopped being a full-height route in #914: the page's root
is an ordinary scrollingflex flex-coland the run detail isstickyinside the
page's own scroll rather than a pane with a scrollbar of its own. One regex was
answering two different questions on the page's behalf, so it is two now:
OWN_SCROLL_PANE(/chatalone, which needs the constrained chain so the transcript
scrolls instead of the page) andOWN_BOTTOM_ROOM(/chatand/runs, both having
something that must reach the bottom edge). Activity then declaresPAGE_CLEARANCE
one level in, on its list column - and that placement is the whole point, because
padding on the box around the two-column row shortens the containing block the
sticky panel is clamped to. Measured at 1440x800 against a transcription of the
page's own chain: room on the outer box gives 64px of clearance and a panel top of
-48px with its header cut off by 56px, which is the figure in the issue; room on
the list column gives 64px and a panel pinned 8px from the window top, header
visible. (#1206)- Below
lg, the run detail panel's last 56px sat behind the mobile tab bar. The
list column ishiddenthere and the panel is the only column, so its flat
h-[calc(100dvh-1rem)]ran under a bar that isfixed bottom-0,min-h-[56px]plus
the safe-area inset. Measured at 390x780: 56px hidden before, 8px clear after, which
matches the 8px it already keeps at the top. The full height stays abovelg, where
the bar is hidden. Same element, two lines, so it is here rather than in a second
change. (#1206) page-transition.test.tsxwas asserting the old reason - "constrains Activity too,
where the list and the run detail scroll apart", true before the page was rebuilt and
false since. It asserts both halves of what is true now: nomin-h-0, and nopb-
either, with the prefix-match case still checking that/runsomethinggets the
room. (#1206)
v0.0.338
Changed
- A presentation and correctness pass over every page of the site - 25 concept and
reference pages, the 8 guides, the 3 reference stubs. The site was accurate and
almost unreadable: 27 pages of unbroken prose, one mermaid diagram between them, no
content tabs anywhere, ~20 pages with no callouts at all, and three flows drawn in
ASCII that only line up in a monospace font. The words are mostly unchanged; what
changes is what a reader sees before they start reading. 17 mermaid diagrams
where prose or ASCII described a flow - the request path and the transaction's
ordering, both ingestion pipelines, the three permission layers, park -> decide ->
resume, the sandbox's three processes, envelope encryption, MCP's OAuth 2.1
handshake, a sync's six stages. ~130 callouts, each promoting a rule the page
already stated and whose violation costs something: a 2xx means the write is
readable, a budget is checked before the request, an empty origin list allows
nothing, the sandbox token is root-equivalent. Content tabs where alternatives were
stacked vertically, and prose restructured where it was a table or a list in
disguise. (#784)
Fixed
- Four pages were teaching things that are not true here.
patterns.md's three
worked examples had all drifted off the code - a DI example injecting
Depends(get_db)andDepends(get_current_user), neither of which exists, where the
aliases do andDBSession'sscope="function"is load-bearing (#353); a repository
written as aConversationRepositoryclass, the one shape the architecture rule
rules out; and a service holdingself.repo, which no service in the codebase does.
howto/customize-agent-prompt.mdtaught editingapp/agents/prompts.pyand
overridingDEFAULT_SYSTEM_PROMPT, with aget_system_prompt_with_rag()and an
AI_TEMPERATUREthat do not exist - contradicting the sentence CLAUDE.md calls the
whole design; it is rewritten around the spec anddefault_instructions.py.
howto/add-background-task.mdstep 2 wasasyncio.create_task(...), which is
exactly the shape #417 was: the task starts before the request commits, so a flow
reading its own row finds nothing, and the exception is dropped too - now
spawn_after_commit/spawn. Andhowto/add-api-endpoint.mdwas a second,
already-diverging copy ofadding_features.md's walkthrough; it is the single copy
now, withadding_features.mdpointing at it. (#784) - Smaller corrections:
configure-sync-sources.mdnamedapp/rag/connectors/twice
for a package that isapp/services/rag/connectors/, and had a sentence ending in a
colon with nothing after it;ROADMAP.mdwas dated 2026-07-27 and contradicted
itself about the 100% gate, with three items describing features that have shipped;
index.mdpromised "four nouns" whereconcepts.mdhas five; and a dead anchor in
configuration.md, which mkdocs reports at INFO so--strictnever caught it.
(#784)
v0.0.337
Added
- A per-conversation approval mode in the chat. The spec decides which tools are
gated, at publish time, per tool - which is right for a statement about what the
agent is, and says nothing about the mood of one session. Somebody working through
twenty turns with an agent that gates three tools answered the same three questions
every turn, and their only way out was to republish the agent, changing it for
everybody, permanently, to fix an afternoon. Three modes ride the send frame beside
the model override: Follow the agent (the default, and exactly what existed
before), Approve everything (standing consent for this conversation - every gated
call granted without parking, each one still writing its row), and Ask about
everything (gate every tool the agent can reach, including the ones the spec left
ungated and the ones no capability owns). (#925) - Four things make it a session setting rather than a hole in the model. A caller who
may not waive is refused, never downgraded - quietly following the spec would
leave somebody believing they had turned the questions off, and the next parked run
says the opposite; the check is inAgentRunnerService.prepare, the one funnel a
fresh run and a resumed one share. Waiving needsapprovals:decideand the
organization's leave: a standing consent is the decision the queue exists to
record, soorganizations.chat_may_waive_approvalsis the ceiling - off by
default, changed by somebody holdingapprovals:decide- and without it a
Builder's deliberate gate onsend_emailwould be one click from nothing in every
conversation. No channel still means no: only the web chat may waive, because
ApprovalGatealready refuses a run with nobody to ask. And every waived call is
recorded - the row is writtenapproved, names the consenting account and carries
decided_via = "standing", its own column rather than a sentence innote, because
a waived run indistinguishable from an agent that was never gated is
docs/governance.md's trail quietly ceasing to be one. (#925) - "Ask about everything" gates MCP tools too. The spec-driven gate leaves them
alone because their approval is a property of the connection; a person who does not
trust an agent yet is asking about everything it can do. It only tightens, so it
takes no permission, no ceiling and no surface check -
ApprovalRequest.capability_idis nullable for exactly this case. (#925) docs/governance.mdgains How much one conversation wants to be asked; the tour
gainschat-approval-mode.
Changed
- New column
organizations.chat_may_waive_approvals, default off, with its own switch
beside the spending limit - so an upgrade changes nobody's behaviour and the waive
option does not render until an owner turns it on.
v0.0.336
Added
- An Owner column on the workspaces table. It said who else could see a
workspace and never who it belongs to:access_labeldescribes the scope -
"everybody who talks to this agent", "one person" - which is a different fact, and on
an agent-scoped workspace shared by six people it is not the one an operator is
asking.owner_labelwas already on the row and already rendered by the chat panel,
used here only as a fallback heading. Plain text and never a link, because
owner_refis a string and a Slack-sourced workspace's owner is a platform id
rather than an account (#131), so a linked cell would be broken on half the rows.
Sortable, because grouping a deployment by holder is what somebody opens this to do.
(#137)
Changed
- One folder tree, not two.
/skillsand/workspaces/{id}had written the same
tree twice - the same recursion, the same expand-collapse set keyed on a folder's
path, the same chevron and two folder icons, the samerole="tree"with
aria-expanded- over two node shapes and two polarities of open state, one
holding what was collapsed and the other what was open.PathTreein
components/filesis now the mechanics and the semantics: indentation by depth, the
roles, one selected file, the open set. What a row says stays with the caller,
because a skill's file is a name and a workspace's is a name, a size and a download -
which is why there are two render props:renderFileinside the button that opens
the file, so that is all a screen reader announces, andrenderFileMetabeside it,
because the workspace's download must not need the file opened first and a button
inside a button is invalid.workspace-explorer.tsxis 110 lines lighter,
skill-files.tsx80, against one 203-line component. One deliberate visual change: a
skill's file rows were indented twelve pixels further than its folders and the
workspace's were not, so the two trees disagreed about the same question. They indent
alike now. (#137)
v0.0.335
Changed
- The Share conversation dialog picks a person rather than asking for an email
address. It had a text field and a hand-rolled suggestion list that appeared only
once something had been typed, so the control's default state was a blank box you
had to already know the answer to fill, and every mistyped address was a 404.
MemberPicker- a popover over acmdklist - opens with the organization in it,
each row a face and a name over the address, and somebody who already has access is
not offered again. The API has always acceptedshared_withbeside
shared_with_email, so this is a client change rather than a contract change, and
sharing outside the organization becomes impossible by construction - #930's client
half.matchingMembersand its four tests go with the field. (#931) - View and Edit carry icons and a sentence.
EyeandPencil, in the select and
on every row, with one line saying what the level permits - because "edit" on a
conversation is not obvious: it is rename, archive, delete and append turns, which
ConversationService._may_writedecides and nothing on the dialog used to say.
(#931) - The access list reads as people.
MemberIdentity, the same row the members
table and the alerts picker draw, resolved against the organization's members - with
a fallback to whatever the share itself holds, because a share whose member is gone
still has to be revocable. The level is the catalog's word now: the badge printed the
API's rawview/edit, so that one row was English in every locale while the select
above it was translated, and the i18n guard could not see it because it is an
expression rather than a literal. (#931) DIALOG_FORMinstead ofDIALOG_CONFIRM, and three separated sections - invite, who
has access, the link - rather than three controls on one row. A share token is not a
person, so it keeps its own row. (#931)
v0.0.334
Fixed
- Every notification link was organization-agnostic, and the page it opens acts on
whichever organization the reader last used.apiClientstamps
X-Organization-Idfrom a selection persisted per browser, so somebody in two
organizations who was last working in Globex opened the approval alert for a run in
Acme and read Globex's queue: very likely empty, and reading as nothing is
waiting about a run that is parked and ageing towards
ApprovalService.expire_stale. The agent links were wrong more quietly -
/agents/{id}under the wrong organization is a refusal for an agent the reader can
genuinely see, one switch away.run.organization_idandagent.organization_id
were in scope at all four call sites and discarded. Every link now carries
org=<id>, built in one place -NotificationService._link, which picks the
separator from the path because the approvals link already carries
?tab=approvals. (#1204) - The console adopts it the way it adopts
/orgs/{id}.organizationInQuery
reads it under the same two rules as the path's reader and for the same reasons
#1032 gives: a UUID only, so a future?org=newis not adopted as a tenant id and
refused on every request, and lower-cased, because the value is stored and found by
===against ids the server serialises in canonical lower case. The adoption is the
existing layout effect in the recovery hook, before the tenant cache reset and
before the page's own queries, so the first request the page makes already carries
the right tenant. Two rules follow from what the parameter is: the path outranks
it, since/orgs/{id}is that organization while?org=only says which one an
alert was about; and adoption is keyed on the path and the adopted id together,
because two alerts about two organizations arrive at the same path and keying on the
path alone would read the first one's tenant. (#1204) - A reader who has since left that organization is told the link is the reason, rather
than being moved in silence and reading another organization's page as the answer to
the alert. It cannot name the organization: they are not a member, so it is not in
their list. (#1204)
Added
docs/governance.mdgains Every link says which organization it is about under
Alerts.