Changelog¶
2026-10-19 — Persona, Settings and Connect wear the A2 Dock design¶
- The last three screens take the warm-paper design, so every screen on iPhone, iPad and Mac now matches Chat. Settings opens with its title in the serif face under a small brand mark; the Connection, Models, Website Access, Appearance and Data & Privacy cards sit on surface with hairline rows, their fields in the sunken well, Connect and Sign in on this device as filled capsules and the other actions as hairline or quiet ones. First-run setup reads "Connect this Mac to your Veetbot." (or iPhone, iPad). The Persona editor keeps its entries as rows of fields in one card, and shows a server conflict or a suggestion from memory as an amber-edged card with Affirm as the filled capsule. Nothing about connecting, models, website logins, task permissions, appearance or the persona changed.
2026-10-19 — Memory, People and Schedules wear the A2 Dock design¶
- Memory, People and Schedules on iPhone, iPad and Mac take the same warm-paper design as Chat and Email. Lists sit on paper with hairline rows: a memory row leads with its kind and the day it last changed, with "Needs review" in amber; the People directory shows initials on a teal wash beside each name; a schedule row leads with its name, state and next run. A memory or a schedule opens as a page with its statement or name in the serif face and its record in titled groups on surface cards. A person's profile keeps its sections, with Prepare for a conversation as the filled capsule and Pin, Rename, Repair identity and Forget person as hairline or quiet ones. Nothing about reviewing, deleting, merging, repairing, importing or forgetting changed.
2026-10-18 — Email wears the A2 Dock design¶
- Email on iPhone, iPad and Mac takes the same warm-paper design as Chat. Each row leads with a Handled circle (the Archive in Gmail checkbox under its reference look), then who wrote and when, the subject, Veetbot's gist, and one line that says what the conversation needs, in amber when it needs you. A thread opens with its subject in the serif face, Veetbot's reading behind a teal rule, the messages as plain prose, a "Teach Veetbot" card for the same feedback as before, and your reply in one bordered box with Review & Send as the filled capsule. On an iPhone the Important / Other mail switch becomes two pills. Nothing about handling, drafts or sending changed: Handled is still reversible, drafts still autosave, and a send still needs your approval.
- The Subscriptions census and the Email learning, Draft history and Review & Send sheets share the same rows, titles and capsule buttons.
2026-10-17 — Veetbot for iPhone, iPad and Mac needs iOS 27 or macOS 26¶
- The app now requires iOS 27 on iPhone and iPad and macOS 26 on the Mac; macOS 27 becomes the floor once hosted CI can run it (ADR-0211). The redesign begun on 2026-10-16 targets the current releases, so the navigation, sheet and People fallbacks for older systems are gone.
- Conversation history on the device is kept by SwiftData alone. The JSON file that older systems used is no longer written; if the SwiftData store cannot be opened, history stays in memory for that launch and refills from the server.
2026-10-17 — Chat wears the A2 Dock design¶
- The Apple apps take the warm-paper palette, serif titles and capsule controls the owner chose on MagicPath, in light and a night-green dark mode; the body face keeps its rounded default. Chat is restyled first: the sidebar lists what needs you (an approval waiting, a report not yet read) ahead of Recent, folders and Scheduled; answers sit on bare paper behind a teal mark and your own messages in teal capsules; each tool run is one sentence that opens into a monospaced ledger; a pending approval or question docks above a translucent composer, and the transcript says "Waiting for your approval" in one amber line. On an iPhone the chat list slides over the conversation. Email, Memory, People, Schedules, Persona, Settings and Connect keep their layouts in the new colours for now.
- The sidebar's Needs you group reads the server's pending approvals every ten seconds, so an approval waiting in another conversation is one tap away.
2026-10-08 — Veetbot flags suspected spam¶
- While assessing new mail, Veetbot now judges whether a conversation is spam: phishing, scams, fake invoices and mass cold marketing from someone you have no relationship with. Newsletters you signed up for and a real person's first contact, such as a founder's pitch, are not spam. Gmail's own sender authentication result is part of the evidence.
- A suspected conversation leaves your priority list, gets no automatic draft and forms no memories. It stays in Other mail with a Suspected spam tag. Nothing moves in Gmail on its own.
- Report spam is now available on any Inbox conversation, and Not spam undoes it. Not spam on a flagged conversation clears the flag. Mail from people you have written to is never flagged on its content alone (ADR-0165).
2026-10-07 — Browser and mailbox recovery stay responsive¶
- New email is discovered even while earlier changed conversations are still waiting to be read.
- A passive reCAPTCHA badge no longer interrupts browser work for sign-in, and reading another page region preserves an unchanged approved draft.
- Verified sign-in can resume a waiting conversation with a live clock. A slow protected-page check no longer blocks other browser sessions.
- Reopening schedule details keeps the newest website-access choices even when an older request finishes later.
- Browser navigation waits for initial data requests even when Chromium reports document readiness before their request events arrive.
2026-10-07 — Conversation titles say what the conversation is about¶
- A chat's title was the first 64 characters of its first message and never changed. After the first reply, Veetbot now asks a model for a short title written from your own messages, and asks again after later replies, changing the title only when the conversation has clearly moved to a new subject. Email threads, schedules and background conversations keep their names. The model never sees Veetbot's replies, tool output or anything flagged as a secret or an injected instruction; a failed attempt leaves the title as it was, and each attempt is recorded without its text (ADR-0155).
- The chat pane's header is gone: the conversation's title is the window title on the Mac and the navigation title on iPhone and iPad, with a line beneath it only when the conversation needs your approval or answer. People and Stop moved to the toolbar.
- Approve, Allow and Deny now show a spinner on the button you pressed and accept no second tap until the server answers.
2026-10-07 — A scheduled report you read on iPad loses its dot¶
- On iPhone and iPad, a scheduled report opened after a visit to Email, or after the app came back from the background, kept its New report dot however long you read it. The app judged whether you could see the conversation from the screen as it was before the switch, so it never told the server you had read the report. The dot now clears as soon as the report is loaded, and new reports appear again without first switching modes.
2026-10-06 — A sign-in check that cannot finish says where it stopped¶
- When Veetbot could not finish checking a sign-in you made on your device, its browser service recorded nothing, so nobody could tell why. It now logs one line that says which of its two page loads stopped the check, what that load was doing, and after how many seconds. The line holds nothing about the website, the page or your session, and the app shows the same message as before. An amendment to ADR-0128 records the change.
2026-10-06 — Choose a schedule's website access in the app¶
- A schedule's detail now has a Website access picker. Choosing one of your ready sign-ins, such as x.com, lets that schedule's runs read the signed-in site; None removes it. The app saves through the existing schedule update and changes nothing else about the schedule. A sign-in that stopped working stays shown with its state, and a refused change is explained while the schedule keeps its server setting. Posting and scrolling still ask first (ADR-0154).
2026-10-06 — Telegram is live, and /pair stays a command¶
- The Telegram channel passed its production smoke: pairing, a run, a question, an approval, the chat commands, and revocation all work from the phone, and no token or pairing code reached a log.
- Sending
/pairfrom a chat that is already paired went to the model as an ordinary message. It now answers "This chat is already paired with Veetbot." and the code is neither checked nor stored.
2026-10-05 — CI checks that the Website Access browser stays quiet¶
- The browser that Website Access uses is kept from contacting Google on its own by settings that a browser update can silently undo. Hosted CI now builds the browser image and watches what its browser asks for while it fills in forms and sits idle, on both processor types. A release waits for that check. It runs again whenever the browser version, the image or those settings change, and is skipped otherwise (ADR-0152).
2026-10-05 — Scale VP data access, built and off by default¶
- Chat can read the Scale VP data service through its REST API once the
owner signs in. A one-time command signs in through the browser and saves
the grant to a private file; a small bridge then offers three read tools
that list, describe and call the service's published read operations,
including its company, contact, deal and investor searches. Nothing is
written to the service, and scheduled runs are not offered the
tools. It stays off until
AGENT_SVP_ENABLED=1names that file. A first sign-in and live read against the real service succeeded the same day (ADR-0153).
2026-10-05 — Website bindings for scheduled briefings¶
- A schedule can explicitly retain its authenticated website profile through
the schedule API. Each occurrence receives that binding and its browser
tools; an unavailable profile fails before spending a model call. Existing
schedules need a full-definition update selecting the profile and requesting
browser.profile.read. Chat creation continues to bind no website, and browser actions keep their ordinary approval requirements (ADR-0150).
2026-10-02 — Website Access verifies sign-in while live updates stay connected¶
- A successful x.com sign-in could still fail verification because its live updates connection stays open. Verification now recognizes an established event stream without waiting for it to disconnect. Ordinary session checks still finish, and the site must still distinguish the imported session from a signed-out browser. ADR-0147 records the approved exception.
2026-10-01 — The Website Access browser stops contacting Google on its own¶
- The full browser that Website Access now uses made its own requests to Google services on every run, apart from the pages it visited. None carried a website's cookies or page text, but one described the layout of the forms on each page. Veetbot now turns these requests off. One remains because the browser has no switch for it: a single request to accounts.google.com when the browser starts, which carries no cookie and nothing about any page. ADR-0146 records the change and the measurements.
2026-10-01 — Website Access works on sites that refuse headless browsers¶
- Some websites, x.com among them, answer a headless browser with an empty "access denied" page on every page. Veetbot's sign-in check and its agent runs used a headless browser, so the check could not confirm a sign-in you had just completed and a run would have seen a blank page. Both now use the full browser, run with a display, that the remote sign-in already used. The browser still reports honestly that it is automated. ADR-0145 records the change.
- The browser service needs more memory for this. Its container limit rises from 1 GiB to 3 GiB, and the production host needs that memory free. At most three such browsers run at once; a fourth run or sign-in waits for one to close.
- A sender can no longer write a link into the email reading view. Only links the reader builds from the message's own anchors and URLs are links.
- A chat no longer fails when OpenAI refuses the optional reasoning summary on the request's last internal attempt.
2026-10-01 — Chats show what the assistant is thinking about¶
- While the assistant reasons before it answers, the Apple app now shows the step it is working on, for example "Thinking: Planning day trips", where it used to show only "Working…". The step comes from the short reasoning summary the model provider streams. It is shown live and never stored. ADR-0144 records the change.
2026-09-29 — A denied approval no longer breaks the chat¶
- After you denied an approval, the next message in that chat failed with an internal error, and so did every later one, including Telegram messages. The denial now keeps the result the assistant was shown, and chats that already hold an old denial accept messages again once this release is deployed. ADR-0142 records the change.
2026-09-29 — New mail arrives during the email catch-up¶
- Email mode reads the last ninety days of inbox mail newest to oldest, a page per refresh. Until that reached the cutoff, new mail was not read at all, so older July mail kept surfacing while fresh mail waited. New mail is now read first on every refresh, and the catch-up keeps its own share of the reads.
- Mailbox status in Email mode shows how far back the catch-up has reached, for example "Checking inbox mail back to Jul 1 — reached Jul 14".
2026-09-28 — Telegram messages are admitted, with their own budget¶
- A paired Telegram or WhatsApp message was refused on PostgreSQL because the default agent has no cost cap to reserve. Each run a paired message starts now has its own budget of USD 10, told to answer once USD 2 remains; chats from the Apple client keep no cost cap.
- The sixteen review findings left open on the surface code are resolved. With
self-approval turned off in the policy,
/approvefrom a chat could still approve your own request; it is refused now./approvewith no identifier crashed, and would otherwise have approved the only pending request; it now matches nothing. - A Telegram message without a readable sender, such as a channel post, no longer stalls every later message. Replies stop retrying after about four hours, surface API responses are never cached, and pairings no longer expose internal identities.
- The Telegram surface runbook walks through creating the bot, installing its token, pairing, and a live smoke.
2026-09-28 — Knowledge search answers questions, and a test-suite review¶
- Searching your documents required every word of the question to appear in one passage, so "When should I water the tomatoes?" found nothing in production. A passage now matches when it shares any meaningful word, as the tests' in-memory store always did; the best-ranked passages still come first.
math.calculateanswered "Infinity" for zero raised to a negative power. It now reportsdivision_by_zero, like any other division by zero.python -m gmail_mcp bootstrapresolved the OAuth client file's path before checking it, so a symlinked client file was accepted although the runbook requires a regular, non-symlink file. It is refused now.- Usage from an OpenAI-compatible server that reports a token count as null no longer fails the reply; a null count reads as zero.
- The in-process run service the evaluation runner uses now ends a run waiting for your answer at once and closes the question, as the HTTP and CLI cancel always did.
- The test suite was reviewed end to end: redundant and vacuous tests were removed or merged, tests that could never fail were repaired, and new PostgreSQL journeys cover a production-shaped Chat across worker restarts, a reminder delivered while you are away, recall in a later chat, chat attachments, and Telegram approval. They found that a paired Telegram or WhatsApp message is refused on PostgreSQL because the default agent has no cost cap; that is recorded as an open item for Milestones 14 and 25.
2026-09-28 — A new chat's MCP records survive a failed write¶
- A new chat holds the records of the MCP servers it connected to and the tool lists it pinned until the chat exists, then writes them in one transaction. A write that failed or was interrupted discarded them for good. They now stay held until a write commits, and each carries a fixed identity, so a retry after a commit whose acknowledgement was lost does not record them twice.
2026-09-28 — Website pop-ups never open, and task approval says what it allows¶
- The website action card's task choice now reads Allow all actions for this task, and the card shows its site scope, time and action limits, and the actions that still ask before you choose it.
- A form in a pop-up window could reach the website while Veetbot was still closing that window, outside the checks every other request passes. ADR-0138: Veetbot's agent browsers now refuse pop-up windows before they exist. Veetbot's remote sign-in browser serves each page with an added rule that forbids pop-ups but keeps scripts, forms, storage and navigation working, alongside the site's own security policy. A pop-up that still cannot be closed cleanly ends the whole browser session.
- The remote sign-in browser now fetches pages through Veetbot's audited proxy with its own HTTP client, so a site that demands a real browser's TLS fingerprint may refuse that remote sign-in. Sign in on this device is unaffected. No approval rule, grant duration, action budget or allowed site changed.
2026-09-28 — A new chat writes its tool pins once¶
- After the warm-up fix, recording which MCP tools a new chat pinned still took about 1.1 s, 0.1 s per server in two transactions each. Each preparation now writes all of its pins in one transaction, and skips tool lists the table already holds, so a new chat's setup should drop by most of that second, both when the chat is created and before its first reply.
agent run latencycounted no chats bound to a browser profile, which is every new chat in the app. It counts them now.
2026-09-27 — Opening an exported file no longer crashes the Mac app¶
- Opening an SVG file attached to a reply could crash the Mac app before the file could be downloaded: the viewer rebuilt its columns when the file finished loading, and AppKit raised on the conflicting widths. The Mac viewer is now one sheet, with the file name on top, a full-width preview, and Close and Download fixed at the bottom.
- A file with no preview, such as an SVG the image decoder cannot draw, shows a short explanation and still downloads with its original bytes. Cancelling the save panel keeps the viewer open, and a failed export shows an alert. Installed apps get the fix from TestFlight.
2026-09-27 — Recent chats stay one tap away¶
- Once chats were filed into folders, returning to the one you just used meant finding its folder. A collapsible Recent chats section below New conversation now lists the five most recently active chats on Mac, iPhone and iPad, including chats inside closed folders. Scheduled sessions are left out.
- They are shortcuts: each chat keeps its folder, and opening one changes neither its place nor its activity. The section starts open, remembers on each device whether you closed it, and also appears with a server that has no folders.
2026-09-27 — Sign-in verification does not wait for every page resource¶
- "I'm signed in" could still fail with "Couldn't check the sign-in" after the browser storage repair: a pending image or embedded page kept the verifier waiting for the full-page load event until its thirty-second deadline expired. Verification now waits for the document and deferred scripts, then performs the existing bounded network-settling check before comparing the signed-in and signed-out pages and capturing the session.
- Verification also waits for outstanding application requests to finish, so a delayed session check cannot make an unfinished page appear signed in.
- Real-browser regressions cover successful handoff and reuse with a pending image or embedded page, rejected credentials, public pages, delayed session decisions, sign-in challenges, redirects, rotated cookies and the overall deadline. The thirty-second deadline is unchanged.
2026-09-27 — Registry changes cannot strand paused chat runs¶
- Releases check that every active run's saved provider configuration exists in the candidate release, then check again with both run workers stopped. An incompatible release is refused and the previous workers resume, so an approval waiting across a deployment cannot lose its model configuration.
- Runs already affected by an earlier registry change explain the cause and ask for a new message instead of reporting an unexpected internal error. Pending tools are not executed and provider pins are never silently changed. Terminal checkpoints discard provider continuation state even when resuming fails before the model loop starts.
2026-09-27 — Large tool results no longer push earlier ones out of a chat¶
- One production chat fetched six pages, the largest 139,764 characters. To make room, the context builder shortened thirteen older tool results, including all eight carried from earlier turns, and the next request read about a tenth as much from OpenAI's prompt cache.
- ADR-0137: the model now sees at most 4 KiB of a tool result
(
output.inline_maximum_bytesin the tool limits), as a head-and-tail excerpt fixed when the result is recorded, so later requests repeat the same bytes. A larger output is still captured whole as an artifact the owner can download, and the stored result keeps its original content for provenance checks. An excerpt can miss a passage from the middle of a long page. - Results already in older chats get the same excerpt on every request,
without rewriting stored history. Each time the builder shrinks a request
it records
context.budget.pressure, including when the shrunk request fits.
2026-09-27 — Research that repeats itself ends with an answer¶
- A research chat failed with
ToolLoopDetectedafter useful work. Large results had been shortened in its context, so the model fetched the same page again until the identical-call limit ended the run, with no answer. - ADR-0136: after four identical calls, the next turn is answer-only. The model is told to give its best-supported answer, name what is missing, and say that research stopped because it was repeating; it cannot call a tool in that turn. The limit of five is unchanged, and a tool call returned in that turn still fails the run before anything runs.
- Budgets, deadlines and cancellation still apply, so the answer may be partial.
2026-09-26 — Device sign-in verification has enough browser storage¶
- After signing in on the device, "I'm signed in" could fail with "Couldn't check the sign-in" because the two verification browsers filled the service's temporary filesystem and crashed. The temporary mount now permits 512 MiB within the existing 1 GiB container memory limit.
- Verification page loads use the existing thirty-second overall budget instead of failing early at a separate twenty-second navigation timeout. Session checks and browser isolation are unchanged.
2026-09-26 — The API starts and stops faster on each release¶
- Every service recompiled its Python code each time it started, because the servers cannot write Python's compiled-code cache. With every service starting at once on a release, the API needed 75 seconds before it answered, and the MCP servers the warm-up starts ran past their handshake limit. Releases now compile the code before they switch over. On the production host that halves the CPU a service or an MCP server spends importing it.
- A stopping API waited for every open live-update connection and was killed after 30 seconds, which held back the new one. It now allows five seconds, and the apps reconnect on their own.
- The API starts the MCP warm-up only once it answers requests.
2026-09-26 — The MCP warm-up survives a restart¶
- Yesterday's change reached production but rarely helped. At each restart the API and the worker warmed up at the same moment as all nine services started, and most server handshakes hit the ten-second limit. The API remembered nothing, so creating a chat still started all eight servers and its first message still waited about 12 s.
- The warm-up now starts one server at a time, waits up to a minute for each
handshake, and retries a server that failed every minute, up to five
times. Nothing waits on it; a chat's own handshakes keep the ten-second
limit.
mcp_discovery_warmup_abandonedin the journal names any server that never answered.
2026-09-26 — Long chats reuse the model's prompt cache¶
- On Claude models no conversation history was ever cached: plans marked only the instructions and the tools, so each request of a long chat paid full input price for everything said so far. Plans now also mark the history the next request repeats, with up to two markers. Open chats gain them at their next request.
- ADR-0132: a scheduled run caches its instructions and tools for an hour instead of five minutes, because it waits on delegated work, devices, the queue and approvals for longer than that. Chats keep the five-minute cache.
- ADR-0135: OpenAI Chat reused almost none of its history either, because OpenAI's automatic cache point followed the working state and recalled memory, which change every step. Requests now carry an anonymous per-session cache key and explicit history cache points. In a live test chat the cost, cache writes included, fell from USD 1.56 to USD 0.37, and reported cost now includes OpenAI's cache-write price.
- None of this changes the text a model reads.
2026-09-26 — Integration tests erase only a database marked disposable¶
- Every integration test emptied every application table in whatever database
DATABASE_URLnamed. CI's URL is the same as the shared local development database's, so a local run with the default URL wiped development data. - The reset now runs only when
VEETBOT_TEST_DATABASE_DISPOSABLE=1. Without it, each case skips and says how to create a scratch database. CI's integration job sets the opt-in for its throwaway container; the README shows the local recipe. - A static test keeps every truncate, table drop, and
DATABASE_URLread intests/integrationbehind the one guarded helper, so a module that brings its own reset fails the fast suite.
2026-09-26 — Chats keep their plan when Veetbot restarts¶
- After a restart, a chat that can use Email feedback or unsubscribe could quietly rebuild its plan at its next message, about half the time. A rebuild recalls memory again and pays full price for the next prompt. In about one restart in 25, that next message failed instead, with "the frozen context prefix no longer matches its plan". Few chats continue across a restart: production recorded two plan rotations of any kind in the last 30 days.
- The cause was the order in which the two Email tools list their permissions. Each worker process could order them differently, and the order is part of the plan's fingerprint. They are now always listed in sorted order.
- A chat whose plan was fingerprinted in the other order, 85 of the 633 that hold these tools, gets a new fingerprint once, at its next message. Its tools, memory and history stay as they are, so an approval it is waiting for still works. ADR-0134 records the change.
2026-09-26 — New chats answer again¶
- After the 2026-09-25 release, the first message of every new chat failed with "the frozen context prefix no longer matches its plan" before it reached the model. Chats started earlier kept working.
- The deferred tool index (ADR-0123) listed each tool's parameters in the order its schema stored them. PostgreSQL keeps no key order inside a stored plan, so the index read back differently from the bytes the plan's hash recorded, and the check that guards the frozen prefix refused the request. Entries now list required parameters in the schema's declared order and optional ones by name, which reads back identically.
- A chat that hit the error recovers on its next message: its plan is rebuilt once and its history is kept. Send the failed message again.
- A PostgreSQL test now sends a new chat's first message with the deferred management tools. Every earlier test of the index ran on the in-memory store, which keeps key order.
2026-09-26 — Each commit answers for its own reading lane¶
- The reading-lane check judged a whole promotion by the newest
Reading-Lane:trailer ondev. A correct lane-B commit at the tip failed hosted verification whenever earlier commits touched lane-A paths, as c50c2df did before PR 135 merged. - Each commit's trailer is now checked against that commit's own changes, and a merge only against what it changed itself. A range whose newest trailer covers everything still passes, so a new lane-A commit still repairs one that under-declared. When a range of several commits fails, the report names each commit that fell short (ADR-0133).
2026-09-25 — Sign in to a website on your own device¶
- Website Access can now sign in on this device. The website opens inside the app, you sign in as usual, and tap I'm signed in. The app hands only that website's sign-in to Veetbot's isolated browser, which checks it from the server before saving it, and then forgets it. Sites that refuse Veetbot's remote browser, such as Duolingo, work this way.
- Signing in again ends any permission Veetbot held to act on that website without asking, so you approve its actions afresh.
- A sign-in you finish in Veetbot's remote browser is no longer lost when you do not come back to the app to check it.
2026-09-25 — Allow Veetbot to finish a lesson without asking at every click¶
- A website action's approval card now says what Veetbot wants to do: the action, the element and the page. The website's own text appears in quotes, labelled as coming from the website.
- When you have turned it on for a site, the card also offers Allow for this task. ADR-0129: Veetbot may then act inside that part of the site, in that chat only, without asking again, for up to thirty minutes, two hundred actions and 4,096 typed characters. A banner above the composer counts them, and Stop ends the permission at once. Activity marks each action the permission allowed.
- Passwords and one-time codes are never typed. Payments, purchases, subscriptions and trials, account and settings changes, messages and posts, deletions, and signing out still ask every time, recognised by the website's own labels. Veetbot's isolated browser checks the live page again before every action it takes under the permission.
- Task permissions stay off until the owner sets the site scopes and turns them on; see Deployment.
2026-09-25 — A chat bound to a website can finish a whole lesson¶
- A chat started with a website profile stopped after about twenty browser actions, because every chat ran under the same 24 model calls. ADR-0130: a chat bound to a website profile now runs under a browser-task budget of 160 steps, 120 model calls, 160 tool calls and USD 30 per run, with USD 3 kept for the final answer. Other chats keep their limits.
- The budget reaches chats started after this release; existing bound chats keep the limits they started with. Standing browser grants must be created again, because the default agent's version changed.
- In a bound chat the three browser tools always keep their definitions.
- Navigation and every action now wait, at most two seconds, for the page to finish loading and stop changing, so the page Veetbot gets back is the one its next action needs and it rarely has to look again.
- Hidden controls no longer crowd out the visible ones on large pages.
- Looking again at a page that changed is no longer mistaken for a loop. A page that never stops changing still ends the run after at most 36 looks.
- Veetbot is told which websites the chat's profile allows.
2026-09-25 — Veetbot keeps a website open while you approve its clicks¶
- With a signed-in browser profile, Veetbot could open a page but not act on it: every browser step started a new blank browser, so a click never found the page it was meant for. One browser now serves a whole run.
- The page stays open while a click waits for your approval, for up to an hour. After that, Veetbot opens the site again. ADR-0127: a run's browser renews while the run is working or waiting for your approval. It closes when the run ends, waits on something else, or is cancelled, so signing in again is not held up.
- A lost reply from the browser service no longer locks the profile for fifteen minutes. A click whose outcome is unknown is never repeated. The next browser step starts from a fresh page.
2026-09-25 — New chats stop starting every MCP server before they answer¶
- Measured over 80 production Chat turns, the first message of a new chat waited 11 s at the median before its first model request, almost all of it starting the Gmail and calling servers two at a time. Follow-up messages waited 0.4 s. ADR-0131: the API and the interactive worker now remember each server's last discovery, a new chat pins that catalog without starting anything, and a server starts on the first call of one of its tools. Both processes warm the memory in the background when they start, so the first chat after a release is fast too.
- Tools stay pinned per chat as before. A server whose tools changed since it
was remembered answers the changed tool with
tool.withdrawn, and the next chat pins the new catalog. Typed Email work still starts its servers at once. - Timing is recorded where it was missing. Each model attempt now records its duration, time to first event, time to first text, retries and usage, and each MCP handshake its duration.
agent run latency [--days N] [--json]prints recent Chat latency from the event log: turn phases, first and follow-up setup, per-model timing and cache use, per-tool and per-server durations, and queue waits. It prints no message, argument or title content.
2026-09-25 — Every person you choose opens, on a redesigned profile¶
- On the Mac, Memory's People collection opened the first person you chose and then ignored the others: the right pane stayed on that profile. It happened once the directory was longer than its column, as a real one is. The directory now selects the person the right pane shows and highlights that row, and each profile starts fresh for its person.
- The person profile is redesigned on Mac, iPhone and iPad. A header shows the person's initials, name, what they are to you and their main address, with Prepare for a conversation, Pin and Rename. Cards follow for names and contact details, relationships in plain words such as "Your colleague" or "Friend of Maya", a history timeline, open threads with their state, and facts with Inspect evidence and Correct. Sections with nothing recorded collapse into one note, and the coverage line says what was analyzed.
- A possible duplicate now asks at the top of the profile. Automatic merges, Repair identity and Forget person sit under Manage person, with Forget in red. Buttons stay legible in dark appearance and at the largest text sizes.
2026-09-25 — Email exchanges are summarized, and Chat can read the original¶
- Asked about someone you had written to, Chat could say only that an email went out on a date to a list of recipients. ADR-0126: each observed email exchange now gets a short summary in that person's history, such as "Sent email: …" or "Received email: …". Each Email refresh writes up to four, newest first, after it assesses mail, prepares drafts and verifies unsubscribe evidence. Each summary is one small model call, billed to the approved Email allowance.
- A summary is kept only when its supporting quote appears word for word in the message or its subject, and when it passes the injection and secret-material checks People already applies to what it shows. Mail older than ninety days, bulk mail, and excluded or suppressed sources are never summarized, and nothing is summarized while email learning is paused. Deleting a fact formed from the same message withholds its summary for good. Excluding or erasing the source, deleting the session that fetched it, or forgetting anyone in the exchange removes it.
people.history@1.1.0names each email's message and Gmail thread, and returns the retained original text of one message, 8,000 characters at a time, to an owner withemail.read. Version 1.0.0 stays registered for chats pinned to it.
2026-09-25 — Duplicate people merge, and teams stop being people¶
- After the People cleanup, the directory still held "Investment Team",
"Partners", a verification service and an address shown as a name, because
the owner had written to them. ADR-0125: a label that is an address, or
that names a group, department, organization or service, never becomes or
matches a person. Rerunning
agent people repair-directoryremoves the existing ones, except any you renamed or confirmed an address for yourself. - Duplicates such as "Erin Vitus" beside "Erin" now resolve in two ways. An address you gave one person, which a correspondent also holds, merges that correspondent automatically, and the survivor's page offers an undo. A name match asks you instead, because two people can share a name. Needs review lists each possible duplicate with Merge and Not the same.
- A pair you keep apart, or a merge you undo, is never suggested again. The
pass runs every 15 minutes, or now with
agent people dedupe --owner TENANT/PRINCIPAL --confirm. GET /v1/people/merge-suggestionsandPOST /v1/people/merge-suggestions/{id}are new, and migration524f16dfc8f9admits the new People record kind.
2026-09-25 — Chat offers every tool, not just the first thirty¶
- Chat sent the model at most thirty tool definitions and silently dropped the
rest: all sixteen Gmail tools, and the next built-in tool would have dropped
mcp.bland_read.list_callsagain. ADR-0123: tools that do not fit are listed in a short deferred tool index and called throughtool.call, under their own names, policy and approvals. Nothing is dropped silently any more; a tool that fits nowhere is recorded on the plan. - Discovered tools now rank reads first, not by name, so looking things up keeps its full definitions. The six schedule and unsubscribe management tools move to the index.
- With production's flags every one of the forty-six tools is offered: thirty definitions at 5,908 estimated tokens plus a seventeen-entry index at 1,013. New chats get it; existing chats keep their roster.
2026-09-23 — Files reach you on the reply, and replies can be copied¶
- A file the agent made never reached the owner:
artifact.exportstored it, but the model saw an empty result and the reply was text only. ADR-0122: every file a run exports now appears as a button under its answer, in the app you already have, and stays for as long as the conversation. artifact.export@2.0.0also takes text directly (plain text, Markdown, CSV, or JSON, up to 1 MiB), so "put that in a file" is one call with no approval prompt. No policy file changed; version 1.0.0 stays for pinned sessions.- The workspace tools and the default instructions now say the workspace is
discarded when a run finishes or pauses and that a file is attached only when
artifact.exportsucceeded. - The Apple app adds Copy and Select Text under every finished message. Copy writes the whole message as formatted text for Mail, Notes, or Gmail, with a plain-text version free of Markdown symbols; Select Text opens a native text view where any range can be selected. The artifact viewer opens at a reading width on the Mac, and the sidebar sync no longer redraws an unchanged conversation every 30 seconds.
- The terminal client adds
/download ARTIFACT_ID [PATH]. - Exporting the same file twice in one run now returns the first artifact
instead of storing a copy, as
builtin-tools.mdalways required.
2026-09-23 — Chat can list calls again¶
- Email unsubscribe added two configured tools, so the owner's roster reached
twenty-eight of Chat's thirty slots. Name order then left
mcp.bland_read.list_callsand every Gmail tool out: Chat could place a call but not list them, and nothing said why. - ADR-0124 takes
workspace.list_filesout of the default agent's roster. The workspace lives for one worker claim, so a model knows what it wrote there; the tool stays registered. All three calling tools fit, at 7,058 of 9,000 tokens. New chats get the change; existing chats keep their roster. workspace.read_textanswers a directory path with "That path is a directory." instead of naming a tool the model may not have.- The production-shaped roster gate now enables email unsubscribe, the flag it missed.
2026-09-23 — People holds who you know or write to¶
- Production People held 130 provisional people, 128 of them names from email bodies: strangers, pronouns, the owner's own address, and other people's relatives. ADR-0121: a person joins only through an owner action, an owner Chat claim that ties them to the owner, or mail the owner sent. Every other mention links to someone already in People or stays unresolved, and its fact still forms.
- Correspondence had never run, because every refresh marks the account syncing and projection required ready. It now needs only the account's address. Mail from an unknown sender records an unattached endpoint, and the owner's first reply adds the person with that earlier mail as history. A gap between two holders of an address creates no duplicate, and ending an alias ends every copy of it.
- Resolution reads each assignment once by exact value, so a frequent correspondent stays matched. Pronouns and the owner's own identities never name a person or reach Chat context. A person the owner creates gets an owner-confirmed name alias.
GET /v1/peopleaccepts a repeatedstateandreview=true. The Apple client's People and Pinned collections list active and provisional people, and Needs review holds only people known by a name or role alone.agent people repair-directory --owner TENANT/PRINCIPALpreviews the one-time cleanup.--confirmbackfills 90 days of correspondence, adds owner name aliases, and removes people nothing the owner did ties to them. It deletes the facts mail formed only about them, which also resets their threads' generated summaries, and keeps facts the owner stated, unlinked.
2026-09-23 — Files can be dropped into the chat¶
- ADR-0120:
POST /v1/sessions/{id}/artifactsuploads one file (at most 32 MiB) underartifact.write, behindAGENT_ATTACHMENT_UPLOADS_ENABLED. Sending the message claims the upload for its run and keeps it for the life of the conversation; an upload never sent expires after 24 hours. - The submit handler now checks every image or file block against the caller's uploads in that session and records the server-detected type.
- The model receives images as images, PDFs as documents, and text inside the untrusted envelope, within a per-request budget the token estimator also counts; anything else is a labelled reference. An artifact reference in a truncated tool result no longer fails the next model call.
- Text, Markdown, and PDF files the owner sends are added to knowledge by the maintenance worker, with the secret scan and a reason code on refusal.
- The Apple client accepts drops on macOS and iPad, and has a paperclip for Files and Photos on every device.
- Deploying: set the flag, grant
artifact.writeandknowledge.write, and deploy the nginx change for the upload path.
2026-09-23 — Choose the chat model and how hard it thinks¶
- ADR-0119: Settings has a Models card on Mac, iPhone and iPad. Chat offers
the deployment's selectable models (
astra,fableandbalancedas shipped, each only when its provider has a key) with the reasoning efforts each accepts. Memory offers only the model and effort combinations that passed the memory-formation evaluation. - A chat model applies to new app chats; existing chats, schedules, email, Telegram, WhatsApp and SMS keep the deployment default. The chat effort applies from your next message, except typed email tasks, which keep the provider's default. A memory choice applies to later formation.
GETandPUT /v1/settings/modelssit under the newsettings.readandsettings.writescopes. A change saved elsewhere in the meantime reloads the current values instead of being overwritten.- Deploying: add
settings.read,settings.writetoAUTH_SCOPES.
2026-09-23 — Answering questions no longer strands a chat¶
- A chat that asked three clarifying questions stayed queued for good: each resume, after an answer, an approval or a delegated child, counted as a crash retry, and the fourth claim was refused. ADR-0118: only an expired worker claim counts toward the crash limit of three. A queued run that does exhaust it now fails visibly instead of waiting.
- The migration rebuilds past crash counts from the maintenance log, so runs stranded this way resume from their saved checkpoint, with their approvals and effect safeguards intact.
2026-09-22 — Bulk mail forms no memory, and flagged memories can be reviewed¶
- The automatic email refresh had formed People facts about a founder named
in a newsletter, from a thread already assessed as low priority and already
in the unsubscribe census. ADR-0116: a census-indexed thread or a
bulkverdict registers no source, forms nothing, and creates no provisional person, in the formation service's source validation so the refresh, the historical import, and any replay meet the same rule; the refresh records a content-freeemail.semantic.skippedevent naming the reason. The assessment prompt is unchanged. agent email exclude-bulkpreviews every retained thread the census indexes and, with--confirm, excludes each through the existing source exclusion. That is coarser than it sounds: list headers also mark group mail, and exclusion drops the thread from the mailbox view. The production cleanup instead deleted the twenty-three bulk-derived beliefs withagent memory delete, which the formation rule keeps from returning.- Every email-derived memory is committed flagged for review, and nothing could
clear the flag or remove a belief from the clients. ADR-0117 adds
DELETE /v1/memories/{id}andPOST /v1/memories/{id}/reviewunder a newmemory.writescope, aflaggedfilter on the list,agent memory review, and native actions: a "Needs review" filter, swipe-to-delete with confirmation, and a Review menu with Mark reviewed, Not true, Not relevant here, and Delete. Hard gate 6 now asserts the exact route table. - Grant
memory.writeto the owner principal'sAUTH_SCOPESwhen deploying.
2026-09-22 — A cleared email failure no longer lingers on the iPad¶
- One refresh failed on 2026-09-17 at 22:48 UTC. Fifty minutes later the iPad still showed "Mail couldn't be updated", although later refreshes had succeeded and new mail was arriving beneath the banner. "Try again" reloaded the inbox but left the banner in place.
- The client cleared a refresh failure only when it watched a later operation
reach
COMPLETED. Production refreshes take 67–94 seconds, and leaving Email cancels the watch, so a short visit never saw one finish. - The client now also clears the failure when an account's
last_synced_atadvances past the value it held when the failure was recorded. The server advances that field only when a refresh completes. A cached read with no newer sync still keeps the failure visible, asemail-experience.mdrequires.
2026-09-22 — A reclaimed run resumes its proposed tool calls¶
- A production run died with an internal
ValueErrorafter its lease was reclaimed: the checkpoint written for the model response carries the turn's tool calls in the conversation, but the loop records them inpending_tool_callsonly at the next checkpoint. A run reclaimed inside that window resumed with calls no result could answer, and assembling the next request raised on the reasoning provider's continuation, which has no trailing tool-result anchor to sit before. - The resume path now reconciles the two representations: trailing tool calls a checkpoint holds without results become the pending batch, so the pipeline re-enters at step 6 and each effect still happens once.
ScriptedTurngainsprovider_reasoning_payload, so the fake provider can return a continuation. Nothing in the suite could reach that branch before.
2026-09-21 — Email can unsubscribe you from bulk senders, off by default¶
- Milestone 31 (ADR-0112) adds a Subscriptions view to Email mode. It lists the bulk senders in your retained mail, how many conversations each sent, when the last one arrived, and how it offers to unsubscribe. There you can Keep a sender, Report spam, or Unsubscribe, up to twenty-five senders at once, and optionally archive their existing mail. A bulk conversation offers Unsubscribe beside its sender.
- Nothing is sent without your tap on the confirmation, or your approval when you ask Chat. Veetbot sends the sender's own one-click request from the server, only for mail whose headers Gmail authenticated, and never follows a link in a message body. A sender that offers only an address gets the email it specified, from your account. An unsubscribe cannot be undone; Report spam can, with Not spam.
- Chat gains
email.subscriptionsandemail.unsubscribe. - It stays off until
AGENT_EMAIL_UNSUBSCRIBE_ENABLED=1, which requires Email mode. The Gmail runbook has the activation and smoke steps, and the privacy policy describes the processing.
2026-09-21 — Scheduled chats group by schedule in the sidebar¶
- A daily schedule added a history row every day. ADR-0113: a schedule with two or more sessions now shows as one collapsible row in a Scheduled section between the folders and the rest of the history, named after its newest session, with its sessions in activity order. A lone scheduled session stays in the history.
- Groups start collapsed and remember your toggles on each device. Scheduled sessions never enter a folder, so their rows have no move menu, and the groups appear whether or not the server offers folders.
2026-09-21 — Paraphrased People labels no longer erase a family memory¶
- "My mom, Cheryl, lives in … Marbella … My brother lives in Redwood City" formed only the request itself. The extractor proposed every fact correctly, but labelled Cheryl's mention with the context "User's mother" and the brother's with the display name "User's brother"; the People linker required both to be exact source substrings and formation discarded each whole belief.
- The linker now drops an unstated context label, displays a paraphrased role
mention by its own span, and formation keeps a belief whose People evidence
is refused, unlinked, counting
people_unlinked. An erased source or identity still rejects the claim.
2026-09-21 — The tool-call budget ends research, never the answer¶
- An interactive research run with 29 of 32 tool calls used asked for five
more; the runtime ran all five, recorded 34 and failed the run, discarding
every result. The loop now fits a batch to the remaining budget before
dispatch and answers each refused call with a
tool.budget_exhaustedresult, so nothing runs unaccounted for. - Reaching
max_tool_callsno longer fails a run before its final turn: the next request is synthesis-only, and a tool call returned in that mode fails closed as the other reserve dimensions do.BudgetScope.TOOL_CALLcarries the pre-call rule for single-call flows (email tasks). RunLimitsgainssynthesis_reserve_tool_calls. The interactive defaults become 24 model calls and 64 tool calls with reserves of 2 and 4, so the model is told to write up before the wall (ADR-0115, amending ADR-0078).
2026-09-21 — Services write structured logs, and the judgment consumers can be read from them¶
- No entry point had ever called the logging bootstrap, so a service ran on
Python's defaults: nothing below
WARNINGwas written, and a warning printed as its bare event name with every field dropped and no redaction. Each long-running service — the API, every worker role, the call worker and the call ingress — now configures logging at phase 1 of its composition root. Standard-library records run through the same processor chain as native events, so theirextrafields, the context variables, the timestamp, the level and the redaction processor all apply. First-party lines are recorded fromINFOand every other logger fromWARNING, which keeps HTTP client request lines, one of which carries a credential in its path, out of the log. One-shot commands are unchanged: their standard output is their result. - The advisory layer writes one
policy_advisory_consultedline per consultation, abstentions included, and an abstention's cause is the provider's reason code when the error carries one. The folder proposal pass writes onefolder_proposal_passline with the audit event's content-free fields. - The judgment port gains a seventh reason code,
judgment.payment_required, for status 402: an account without credits no longer reads as a malformed request. - A static test refuses a log call whose
extranames a field the log record reserves, which raises only once its level is enabled.
2026-09-21 — Two simulator UI cases no longer depend on timing¶
- On the hosted iOS 27.0 simulators
testEmailThreadFeedbackEditingAndExplicitSendandtestMemoryBrowserListsAndOpensDetaileach failed once and passed on a rerun; the second failure, on the merge commit of #127, held back its deployment until the rerun. - The email cases waited five seconds for a Gmail archive outcome that the fixture and the client's one-second poll make take at least three. They now allow twenty.
- The sidebar menu helper tapped an item while the menu was still opening, and the simulator dropped the touch. It now waits for the item to stop moving and requires the menu to close, tapping again up to three times.
2026-09-20 — The simulator UI lane boots both simulators before its runs¶
make test-apple-ui-ioscould end before any case ran, with the UI-test runner's launch refused as Busy. Xcode 27.0's xcodebuild installs the runner early in a boot it starts itself, and two cold boots at once bring SpringBoard up after that install.- The target now boots both simulators to completion, concurrently with
xcrun simctl bootstatus -b, before it starts the two concurrent runs, and shuts down only the simulators it booted, as xcodebuild did. A boot that does not report "Finished" waits untilcom.apple.springboard.finishedstartupholds SpringBoard's process id. - Six of six pre-booted concurrent runs passed on the owner's Mac. The
diagnosis is in
clients/apple/README.md.
2026-09-20 — The hosted Apple lanes pass on the Xcode 27.0.0 image¶
- The first hosted run on CircleCI's Xcode 27.0.0 image failed both Apple jobs. That image runs macOS 26.6.2, not macOS 27, and carries the iOS 27.0 simulators, which the simulator lane now selects as the newest runtime. The 2026-09-19 entry's claim that hosted runs and the owner's Mac exercise the same system behavior was wrong.
make test-apple-ui-ioswas ended by CircleCI's ten-minute limit on a step without output. Under Xcode 27, xcodebuild finished both simulator runs withsimctl diagnose --timeout=600, which printed nothing and then reported "Timed out after 600.0 seconds" with no diagnostics collected. The simulator runs now pass-collect-test-diagnostics never; the result bundle still holds each failure, its screenshot and the element tree.- The adjacent-folders Mac case bounded the space between two folder labels at 12 points. That space is the system's row height less the system's label height: 16 points on the hosted macOS 26.6.2 runner and 13.5 on macOS 27, with the folders correctly sharing one section. The case now requires the two list rows that hold the folders to touch, which fails by the 13-point section gap when each folder sits in a section of its own and holds on both releases.
- On the iOS 27.0 iPad simulator the Memory browser case failed because XCUITest's element tap on the overflow menu's first item was swallowed and the menu stayed open, three runs of three; the same case passes on the iOS 26.5 iPad simulator. Taps at eleven points across that item, its centre and the point over the covered Hide Sidebar button included, all opened the browser, so the item responds to touch. The cases now tap an overflow item at its centre after requiring it to exist and be hittable.
2026-09-20 — An offline email-importance replay compares production with a judgment arm¶
agent eval email-importancechecks a private source bundle, runs the replay, or compares two results. It is the ranking producer Milestone 26's quality gate lacked, with the production assessor as one arm and the typed-judgment port as the other. It is non-activating and can satisfy or waive no gate.- Each frozen snapshot is replayed in a fresh composition at its observation
time, with profile evidence cut at its declared date and no later message
imported. Both arms run the production
assess(); the judgment arm replaces only the model call. - Four arms separate what a judgment provider cannot represent, a future expiry, from what it can. The report states its limits in its header, and the decision rule is declared before any provider call.
- Artifacts are label-only. A malformed bundle is refused without quoting it, and the replay is a boundary of the email privacy gate.
- The real run waits on the owner's frozen labelled corpus and source bundle.
2026-09-20 — The advisory approval layer lands, off, observing before enforcing¶
PolicyAdvisoris a port that receives only the proposed action, andAdvisedPolicyEngineconsults it only on a plain allow for a web search, a page fetch or a browser navigation. It can only escalate to an approval, never denies as shipped, and abstains on a timeout, an error or a missing provider.- The first advisor asks the typed-judgment port three narrow questions about redacted, escaped and delimited outbound arguments; any one signal escalates. Its thresholds are untuned initial values.
- The tool pipeline holds the deterministic engine as its recovery policy, so a resumed or re-approved invocation never asks the advisor twice and an escalation recorded before a crash survives it.
- ADR-0111 is amended: observing is the environment flag
AGENT_POLICY_ADVISORY_OBSERVE_ENABLED, which moves no policy version; enforcing is the profile valueadvisory.enabled, which does, and so unbinds the memory-formation release evidence until it is regenerated. The shipped profile is byte-identical. - The five
gate.policy.advisory_*gates are bound to executable checks.
2026-09-20 — The judgment matcher proposes add-to-folder matches, default off¶
- An optional matcher wraps the chat-folder grouper and asks the typed-judgment
port one closed question per unfiled conversation: which existing folder it
belongs in, or none. A match is accepted only above
proposals.judgment_match_threshold(0.8), and it only ever becomes an add-to-folder proposal the owner accepts or declines. - A conversation with a hazardous title is not judged, hazardous snippets and sample titles are omitted, option keys are opaque, and no identifier leaves the process. Any provider error, deadline, or budget breach discards every judgment and leaves the existing grouping unchanged; the first request runs alone so an outage costs one call.
folder.proposal.passalways carries eight content-freejudgment_*fields.- It needs
JUDGMENT_PROVIDER, the key, andproposals.judgment_matching_enabled, which ships false. The knob census is 179.
2026-09-20 — The typed-judgment port and the TypeSafe adapter land, default off¶
agent_core.ports.judgment.JudgmentProvideranswers closed, typed questions over frozen domain types: the probability that a statement holds, a choice among offered options, and a position on ordered levels.- The TypeSafe adapter speaks raw HTTP to one fixed endpoint, follows no redirect, resolves its credential on every call, bounds request and response size, retries only transient failures on the injected clock, and raises one typed error whose message is a reason code. No failure carries a response body, request state, or the key. Cost is computed locally at the pinned, dated price.
JUDGMENT_PROVIDERselects it and defaults todisabled; a key alone enables nothing, and a selector without a key warns once and never refuses startup. No consumer exists yet, so no judgment request is made anywhere.- The four
gate.judgment.*gates are bound to executable checks.
2026-09-19 — ADR-0111 authorizes Milestone 30, the advisory approval layer¶
- The owner authorized the restrictive-only half of roadmap item B8: an optional advisor that can only turn an allowed action into an approval request. ADR-0017 accepted the idea in July and the policy specification designed it; nothing had been built. General standing approval grants stay on the roadmap.
- The advisor is a port, and its first implementation uses the typed-judgment port rather than the model gateway, a divergence from the plan's sequencing table that ADR-0111 records. It is consulted only on allowed web searches, page fetches and browser navigations, where private data can leave inside a query or a URL; it never denies, abstains on any failure, and observes before it enforces.
- The specification's claim that the layer needs no caller change is corrected: the tool pipeline gains a deterministic recovery policy so a resumed or re-approved action never asks the advisor twice.
- Five
gate.policy.advisory_*gates are registered pending at Milestone 30. Nothing is implemented.
2026-09-19 — ADR-0110 admits a typed-judgment port under Milestone 29¶
- The owner authorized a provider-neutral port for closed, typed questions —
the probability that a statement holds, a choice among offered options, a
position on an ordered scale — with TypeSafe's Jev as its first provider.
typed-judgment.md is its design. It is not a
model-gateway provider, registers no model profile, and is off by default
behind
JUDGMENT_PROVIDER. - Its first consumer is an optional judgment matcher for chat thread folders: one closed choice per unfiled conversation over the existing folders and an explicit no-match option. It only ever proposes, and any failure leaves the existing grouping unchanged.
- The same ADR admits an offline, non-activating email-importance evaluation under Milestone 26 and records the owner's acceptance of the vendor's published data terms, which state no deletion period.
- Five gates are registered pending, four
gate.judgment.*andgate.folder.judgment_matching. Nothing is implemented.
2026-09-19 — The Mac UI lane runs again on macOS 27¶
- Every
make test-apple-ui-macoscase failed after the macOS 27 upgrade: the application under test opened no main window, so every query timed out. XCTest ends the application without quitting it, so AppKit kept the window list each launch had saved. The first launch after the upgrade could not restore the window saved before it, saved an empty list in its place, and every later launch restored that empty list and opened nothing. Mac cases now launch with-ApplePersistenceIgnoreState YES. - That pair comes before the
--ui-testing-*flags. AppKit reads launch arguments as-key valuepairs, so a flag ahead of it takes the key as its value and AppKit opens the leftoverYESas a document, which also opens no window. The same pairing left the application windowless on 2026-09-15. - macOS 27 floats a Writing Tools affordance beside the focused composer, over controls the cases click and hit-test. The composer turns Writing Tools off under the UI-testing fixture only.
- macOS 27 also reports SwiftUI message text as disabled, and reports a scroll child clipped out of view as hittable. The mixed-tool summary and People identity cases check frames instead of hittability.
- macOS ignores the touch-style press-and-drag on a divider; the mouse click-and-drag still moves it. The email sidebar case uses the mouse gesture.
- CircleCI's four Apple jobs move from Xcode 26.6.0 to 27.0.0, so hosted runs and the owner's Mac exercise the same system behavior.
2026-09-19 — Every call-result webhook leaves a trace¶
- Nothing recorded whether Bland's post-call webhook ever arrived or passed its signature check. The proxy route deliberately keeps no access log, the listener runs without one, and a rejection returned 401 silently. Every result so far arrived through the worker's polling, so the gap was invisible.
- Each delivery now writes one content-free warning:
calling.callback_accepted, orcalling.callback_rejectedwith a fixed reason. A missing or malformed signature, a mismatch, an invalid payload, an oversize body, a timeout and a full queue each have their own reason. No body, signature or call identifier is logged.
2026-09-19 — Chat-created schedules no longer pause on their first failure¶
- The two recurring briefings failed six times between 2026-09-01 and
2026-09-17, for unrelated reasons that were each repaired, and every failure
paused its schedule until it was resumed by hand, up to seven days later.
schedule.createhad pinned the one-time-reminder value of one allowed failure onto recurring schedules. A schedule created in chat now tolerates one failed occurrence and pauses on the second consecutive failure. - Chat-created schedules had also pinned a USD 1 budget with no final-synthesis reserve, the configuration that failed the 2026-09-01 briefing. They now get USD 5 with a reserve of 2 steps, 2 model calls, and USD 1, so a research run near its budget writes its answer instead of failing.
- The cause of three failures was lost when their conversations were deleted.
schedule.run_accountednow keeps the failed run's reason, error class, and sanitized provider code, without any run content. - Existing schedules keep their pinned values until an API client revises them (ADR-0109). Both briefings were revised to these values through the API on 2026-09-19. Each still carries one failure from its last pause, which its next completed run clears.
2026-09-19 — Chat folders stay as the owner leaves them, one open at a time¶
- Folders no longer spring open when Veetbot proposes a new one. Which folders are open is remembered on each device and changes only when the owner opens or closes one; a proposal, a refresh, or a new folder leaves it alone.
- Solo mode, on by default, keeps one folder open at a time: opening a folder closes the one that was open. A right-click on a folder on the Mac, or a long press on iPhone and iPad, turns it off. Clicking a folder's row opens or closes it.
- A suggested folder lists each conversation it would file on a line of its own, so the proposal can be read on an iPhone, and a new-folder suggestion has a Rename… button that accepts it under the owner's name. A name that is already taken is shown in the sheet and the suggestion stays open; accepting without renaming now reports a taken name instead of doing nothing.
- On the Mac, the folder name sheet is a compact dialog rather than a split view with a cramped field, and folders are spaced like conversations rather than each sitting in a section of its own.
2026-09-19 — Willow uses the owner's introduction and can leave a voicemail¶
- Calls open the way the owner chose: "Hi, this is Willow, Andy's assistant." Neither direction announces AI identity or transcription unprompted. Asked directly, Willow says truthfully that she is an AI assistant and that the call is transcribed for Andy. The profile generator reproduces the owner's live inbound prompt, so the configuration readback can match again (ADR-0108).
- An approved outbound call can leave a voicemail.
start_calltakes an optionalvoicemail_messagethat the owner approves word for word with the brief. Without one, the call hangs up at voicemail, as before, but now by explicit instruction. The SMS variants stay refused. - Outbound calls wait for the other side to speak first, so Willow no longer talks over a greeting or iPhone call screening. The first no-answer test was cut off that way.
2026-09-18 — One malformed model response no longer fails an Email refresh¶
- A refresh at 22:48 UTC on 2026-09-17 failed as an internal error after one assessment response did not match its schema. The parse raised a raw validation error outside every per-thread guard, so the remaining threads were not assessed and the client reported that Mail could not be updated.
- A rejected response (invalid JSON, out-of-bound values, tool calls or no result) now records the same abstention as ungrounded evidence for that thread, keeps none of its text, and lets the refresh continue. The next refresh retries that thread once; a second rejection is kept until the thread is next invalidated. A rejected automatic draft leaves its thread undrafted. The attempt still settles, and an owner-requested draft still fails. The Email experience design states the behavior.
- The run's response event had still recorded the text and arguments of any tool calls in the rejected response, which can repeat the mail that induced them. Email assessments and drafts now keep only the tool names there, and the hostile-mail privacy gate checks it.
2026-09-18 — Chat remembers the people it is told about¶
- Every chat consolidation since People formation (
formation@11) became the default failed its distillation call: four optional time-zone and precision fields made OpenAI reject the structured-output schema asinvalid_json_schema. Only the local fallback ran, so "Kyrri and Riv are my daughters. Erin is my wife" formed a single nameless "User has a wife." The fields are now required, and the guard test that caught the same fault information@9covers the People schema too. - In the same turn, all three explicit saves were refused as untrusted. Recalled
memory in context runs the turn at memory trust, which
memory.rememberaccepts; its person-linked version demanded user trust and so could not succeed for an owner with memories. It now follows the base tool: memory trust is accepted as an affirmed statement, external content is still refused, the owner's message must still name each person, and an over-portable relationship gets the same retryable refusal.
2026-09-17 — One unusable assessment no longer fails a People email import¶
- A historical email import parsed each passage assessment without a guard. A
response that failed its schema, was malformed or truncated JSON, or asked
for tools raised outside the import's handlers. The run was recorded as an
internal error and the whole job failed as
run_interrupted, with no failure counted. The defect was found by reading the code, not from a reported failure. - Such a response now pauses the job as
analysis_incompletebefore that passage and keeps none of its text, as a Chat source already does. Its cost stays charged to the job, the run and the email allowance. Resume reassesses that passage alone. The failure counts once and clears when a retry succeeds. The People design states the behavior.
2026-09-17 — Hosted CI runs on main and on request¶
- A push to
devor any other branch no longer starts CircleCI. Over the previous week 96 of 122 pipelines weredevpushes at roughly 3,900 credits each, about 96% of it macOS executors. - Branches are verified with
chunk validateon the sidecar. Before a merge tomain, one pipeline triggered withrun_verify: trueruns the full hosted workflow on the final head.mainstill verifies and delivers on every push (ADR-0107).
2026-09-17 — Call results reach the lock screen¶
- The lock-screen alert builder had no case for
call_finished, so it raised before the push was sent. The dispatcher records any such failure asTransportError, indistinguishable from a network fault, and retried all five devices until the notification would expire thirty days later. The first real call result, on 2026-09-17, was never delivered. - The alert now reads "Open Veetbot to read the transcript and summary", with no caller, number or summary on the lock screen. A guard test fails when any notification kind has no alert case, and the dispatcher logs the underlying exception instead of discarding it.
2026-09-17 — Chat reaches the call tools again¶
- The owner's configured tools reached twenty-six items and 5,973 of the 6,000 tokens the tool-definition class allows, so every discovered tool was skipped while four of the thirty slots stood empty. Chat could not list or place a call, and no error said why. A message left with the receptionist on 2026-09-16 was received and stored, but nothing could show it.
- ADR-0105 raises the class to 9,000 tokens and the prefix ceiling to 20,000, so
the thirty-tool item cap binds first. Builder version
context-builder@11rebuilds existing plans through the ordinary epoch rotation. - A production-shaped roster case now gates the calling tools.
2026-09-17 — Scheduled runs start again¶
- No scheduled run could start after the People memory release. Session
history now locks event rows while it builds, so an erasure cannot race it.
That lock needs
UPDATEonevents, which the scheduler's database role does not have. On 2026-09-17 the 09:00 PDT weekday briefing retried every five seconds onpermission denied for table eventsuntil its late-start window ran out. - The scheduler now skips that lock only for the session its own transaction just created. Erasure cannot see that session until the transaction commits. The scheduler's permissions are unchanged, and it still cannot build history for any other session.
- A new integration test runs the production scheduler through a login that holds exactly the permissions the release check allows. Earlier tests ran the scheduler as the database owner.
- A schedule that paused after the missed run needs to be resumed.
2026-09-16 — Chats can be filed into folders¶
- Milestone 29 (ADR-0102): the Apple sidebar can hold folders you create, rename and delete, and each chat's move menu files it into one. Folders live on the server, so a move on one device reaches the others.
- The maintenance pass proposes a folder once several unfiled chats belong together, at most three proposals at a time. You accept, decline, or rename a new folder as you accept it; nothing is filed without your acceptance, and a declined grouping is not proposed again.
- Only chats can be filed; email, scheduled and delegated sessions cannot.
The routes use the existing session scopes and stay off until
AGENT_THREAD_FOLDERS_API_ENABLED=1.
2026-09-16 — Veetbot remembers the people in your life¶
- Milestone 28 (ADR-0100, ADR-0101) adds People: who someone is, how they relate to you and to each other, facts about them, dated interactions and open commitments, each tied to the chat or email it came from. Chat forms People memory from what you tell it, Email from your correspondence within its 90-day window, and both recall it when a person comes up.
- The People collection in Memory, on Mac, iPhone and iPad, shows each person's evidence and lets you correct a fact, merge or split identities, undo a repair, forget a person, and import older chats or mail under a date range and a spending cap.
- People is on by default; grant
people.readandpeople.writeto the owner. The People operations runbook covers imports, erasure and rollback.
2026-09-16 — Call roles skip the client-certificate probe¶
- The call worker crash-looped after the first calling release. Its unit hides
home directories, and without
PGSSLMODE=disablethe database driver looked for~/.postgresql/postgresql.key, was refused, and exited. Both call role examples now setPGSSLMODE=disable, like every other database role. - The release now waits 90 seconds before checking the calling services, up from 15. Under deploy load, the worker's first crash came after 64 seconds, so the shorter wait passed a failing unit.
2026-09-16 — The webhook listener's release grant comes last¶
- The first calling release failed its own check: the webhook listener could
not read
.venv/bin/agent. The deployment check'suv runhad reinstalled the project's entry points after the release granted the listener access. The release now grants that access as its last step before promotion.
2026-09-16 — Email tasks start only the Gmail servers they use¶
- An Email archive now starts only the account's read and write Gmail servers, not all six. In production, starting all six took 17.7 seconds of an archive run, and closing them took another 4–7 seconds before the worker took its next task. A refresh starts only each account's read server. A send starts the read and send servers. A draft starts no server (ADR-0104).
- A thread's first Email task still starts every server, because Chat reuses the tool list it records.
2026-09-16 — Calling services start and are checked on release¶
- The call worker and webhook listener no longer refuse to start with their shipped settings. Those leave the owner's scopes empty because neither service uses them.
- The webhook listener's account can now run the released code. When intake is enabled, the release grants that one account read access to the new release, which is otherwise closed to accounts outside the application group.
- A release that enables calling now waits 15 seconds after starting the calling services and fails if either one is down or has restarted, instead of reporting success while it crash-loops.
- The deploy account's sudo contract gains the matching status rules. Hosts with an older contract need it reinstalled before calling is enabled.
2026-09-16 — Bland calling setup passes the release preflight¶
- The Bland setup guide now gives file permissions that the release preflight can check. Before, it made the listener's environment file readable only by the listener, so the deploy user could not read it. It also left the secret directory closed to the deploy user. A production release failed with a misleading "must match the application environment" error.
- Both calling environment files are now
root:veetbotwith mode 0640, and credential directories use mode 0711. The webhook secret and the Bland API key stay mode 0600 and readable only by their services. The listener still cannot read the application environment. - When a calling environment file is unreadable or a credential directory
cannot be traversed, the release now names that path. The release test
replays the guide's commands as both the
veetbot-deployandveetbotdeploy users.
2026-09-16 — Email archives no longer wait behind refreshes¶
- An Email-mode archive or move-to-Inbox run now uses the interactive queue class. Previously it shared the single asynchronous worker with refresh runs, which take about two minutes each. On 2026-09-16 one production archive waited 69 of its 115 seconds in that queue. Refresh, draft and send tasks remain asynchronous. Consent, fencing, approval and uncertain-effect checks are unchanged.
2026-09-16 — Gmail request URLs stay out of service logs¶
- The Gmail MCP servers no longer write a line for each Google request to the API and worker journals. Those lines exposed search queries, which can hold names, addresses and subject text, as well as Gmail thread and message IDs. Warnings and errors from the servers still reach the journal.
2026-09-16 — Cheaper Email listing, admission and body retention¶
- The Email inbox list reads thread summaries without message content, and PostgreSQL drops the bodies in the query. On a PostgreSQL copy with 4,044 production-sized threads, a priority page fell from about 850 ms to about 260 ms. Owner feedback is sorted only for the threads it matches.
- Admitting refresh, draft, send and archive operations, and endorsing a writing example, no longer scan every cached thread, draft and draft revision first. On the same data, admission fell from about 800 ms to about 4 ms.
- The maintenance worker's email body sweep runs once an hour instead of on every five-second pass. The thirty-day body window is unchanged: thread and draft reads, refresh selection and imports withhold expired bodies before the sweep reaches them, and never send one to the model.
2026-09-16 — Bland request URLs stay out of service logs¶
- The Bland MCP servers no longer write a line for each Bland request to the API and worker journals. Those lines exposed provider call IDs, the configured phone number and call-history query values. Warnings and errors from the servers still reach the journal.
2026-09-16 — Email reads no longer wait for archive and refresh admission¶
- Archiving a conversation or starting an Email refresh no longer starts the Gmail MCP servers in the API process. In production that startup took 10–16 seconds per request and ran while the owner's email lock was held. Thread reads and inbox reconciliation waited behind it, and archive requests took 23–25 seconds. These requests now finish without that delay. The worker still starts its own servers when it runs the task (ADR-0103).
- Opening a thread in Chat or starting its first draft still pins the full skill catalog, but server discovery now happens before the lock is taken.
2026-09-16 — Faster native Apple CI lane¶
- The Apple CI gate now runs on two macOS executors at once:
appleruns the Swift unit tests and the macOS UI cases, andapple-iosruns the iPhone and iPad cases. Release packaging requires both. No test was removed. make test-apple-uinow runs the newmake test-apple-ui-macosandmake test-apple-ui-iostargets in turn, so either platform family can run alone.- UI cases set their fixture options before a single launch instead of
launching in
setUpand relaunching, which saves one app launch per affected case on every destination. - The Email status-polling test runs its bounded backoff in virtual time and
also asserts the backoff delays, removing about thirty seconds of real
waiting from
make test-apple. - Each Apple job uploads its result bundles as one archive instead of thousands of files.
2026-09-16 — Archiving the open email from its row advances the reading pane¶
- In the Apple client's Email mode, checking off the open conversation in the inbox list now clears the reading pane and opens the next visible conversation, or the previous one at the end of the list, as the reading pane's own checkbox already did. Previously the archived conversation stayed open until another row was chosen. Archiving any other row still keeps the current selection.
2026-09-16 — Adding a person no longer adds a conversation¶
- The conversation index hides the sessions that anchor native People writes
(
purpose: people-management) and People import workers (purpose: people-import). Previously every import preview, and every Add person or profile edit made without a selected conversation, left an empty conversation in the sidebar. - The Apple client prunes those sessions from its cached sidebar. The sessions stay on the server because they hold the evidence behind each owner-added person; deleting one would erase that evidence.
2026-09-15 — Personal-context memory retrieval repair¶
- Session snapshots exclude provisional beliefs before candidate limits, keeping recent uncertain profile entries from crowding out confirmed personal facts. Provisional beliefs remain available to in-turn recall and deliberate search.
- Automatic and deliberate belief searches ignore common English function words, so matches on "the" alone cannot fill the recall budget. Literal browsing and knowledge search retain their existing matching behavior.
- Explicit questions about the owner's profile combine words with bounded structured lookup, so paraphrased personal details remain retrievable without relying on provisional snapshots. The unchanged 66-probe benchmark recalls 58 needed facts instead of 57, with irrelevant recalls falling from 159 to 119 and no per-probe recall regression.
memory.searchguides the model to look up missing personal context before asking the user to repeat it, using the needed fact rather than only the topic of the question. Existing session snapshots remain frozen for their context epoch.
2026-09-14 — Veetbot can take and place phone calls, once set up¶
- Milestone 27 adds Bland calling, off until the owner configures it. Callers to the public number reach an assistant that takes a message and answers only from a reviewed public profile. Chat can place an outbound call only after you approve its exact brief, and can list and read the calls it keeps.
- Each finished call's transcript and summary arrive through a signed webhook, stay for thirty days, and can be deleted, which also redacts what they produced in chats. A notification can announce a finished call.
- The Bland setup guide covers the credentials, service roles, database grants and flags.
2026-09-14 — Websites load their scripts and images without setup¶
- Website Access profiles no longer need extra CDN origins. The isolated browser loads a site's public HTTPS scripts, styles, images, fonts and embedded verification frames through an audited proxy, while navigation stays on the profile's own origins and private networks stay blocked (ADR-0098).
2026-09-13 — Website Access redirect failures are actionable¶
- A website that redirects the login page to an origin the profile does not
list, such as a bare domain redirecting to its
wwwaddress, now returns a400 malformed_requestthat explains the fix instead of a500internal error. The isolated browser runtime reports the refused redirect astool.browser.url_disallowedand other launch navigation failures astool.browser.provider_unavailable; no raw browser text leaves the service. - The rejected attempt creates no ceremony record and leaves the profile available for a corrected retry.
2026-09-13 — Immediate email archiving¶
- Checking Archive in Gmail removes the email immediately while archiving finishes in the background. Routine progress and success messages are silent.
- Other emails remain actionable; failed or uncertain archives return to the list with an error. Refreshes preserve pending removal and unsaved replies.
2026-09-12 — Faster local verification¶
make checkoverlaps two independent jobs, and local static tests use the same two-worker configuration as CI. Contract tests remain serial and follow static tests; the complete check set is preserved.- Website verification reuses a healthy local dependency installation when its
manifests, runtime, and npm configuration match. CI still installs cleanly,
and
WEBSITE_INSTALL_FORCE=1forces a local reinstall. - Agent instructions require one final aggregate verification and measured timing reports before commits, avoiding separate repeats of included checks.
2026-09-11 — Astra and Fable model defaults¶
- New production sessions default to the
astrapolicy, selecting OpenAI GPT-6 Astra;flagshipandfableselect Claude Fable 5.1. GPT-5.6 Sol and Claude Opus 5 retain their model IDs and named aliases. - Updated the new models' price snapshots. Astra remains within the existing 272,000-token standard-price boundary; Fable retains a one-million-token window. No provider adapter or reasoning settings change.
- Memory formation independently retains
balancedon GPT-5.6 Sol under ADR-0093, preserving its exact activation evidence. The initial Astra evaluations failed existing gates; wording mismatches, omissions, duplicate memories, and classification differences need further work. Selection audits distinguish chat and memory policies. Existing sessions retain their recorded model policy; new sessions receive the changed chat default.
2026-09-10 — Current schedules and recent terminal history¶
- The native schedule browser now defaults to ACTIVE and PAUSED records and offers a separate Recent History section for COMPLETED and CANCELLED records. Both sections use server-side lifecycle filters before cursor pagination.
- Terminal schedule records, immutable revisions, occurrences, and schedule idempotency rows are retained for thirty days and then removed by bounded, fleet-safe maintenance. Linked durable runs, sessions, and content-free audit events retain their independent lifecycles.
- Recurring cadence summaries omit the time zone when it matches the device's current zone and retain it for exceptional cross-zone schedules.
2026-09-06 — Full production tool roster fits the context guardrail¶
- Removed the redundant prose copy of every provider tool name and compacted the model-visible recurring-schedule schema without changing its closed cadence validation. The normal Gmail, scheduling, and web composition now advertises all 29 tools below the existing 6,000-token tool-definition cap.
- Added a production-shaped interaction regression and advanced the context builder version so existing sessions rotate cleanly to the repaired prefix.
2026-09-04 — Schedules can be revised in conversation¶
- Added approval-gated
schedule.updateso a conversation can revise a schedule's title, instruction, or supported one-time/recurring cadence using its stable identifier and current revision. Updates preserve the schedule's hidden execution authority and limits, keep prior revisions immutable, and recompute only a future active firing; paused schedules remain paused. - Added retry-safe update semantics, stale-revision and terminal-state refusals, secret scanning, feature-gated registration, and five Milestone 23 gates under ADR-0088.
2026-09-04 — Memory snapshots fit their final context class¶
- Memory retrieval now applies the context planner's model-specific token
measurement to the complete rendered snapshot, including the memory wrapper,
trust envelope, and serialized message framing, before it records the faithful
recall trace. Verbose accumulated memories therefore trim at the configured
boundary instead of making a new session fail with
context_overflow. - Scheduled sessions now use the configured async snapshot profile, delegated
sessions use the child profile, and all session classes enforce their absolute
item and token caps together with the two-percent model-window ceiling. Public
session creation rejects the scheduler-owned
schedule_idand delegation-ownedrun_kindmetadata keys, so an interactive caller cannot select another run class's profile.
2026-09-03 — Repaired provider-assisted memory formation and policy-bound evidence¶
- Added
formation@10under ADR-0086: the provider-assisted extractor's five-cent budget, byte-per-token preflight, and most-recent belief view had starved every call on a populated store, which the production audit of the strength-training conversation confirmed.formation@10keeps one call, bounds input by the model window, requests 16,384 output tokens, records cost without gating on it, and sends a relevance-ranked, sensitivity-filtered belief view. Automatic selection prefersformation@9, thenformation@10, then the frozenformation@8; the operator pin accepts all three. - The bundled
formation@9artifact was found bound to the pre-Milestone-24 compiled policy version, so the first deploy silently ran deterministic formation. The evidence was regenerated on the deployed tree, and the bundle test now fails any artifact whose policy binding the tree no longer compiles. The staleformation@8artifact is withdrawn. - A digit range and a spelled-out range of the same count now merge across claim kinds, closing the duplicate the production replay exposed.
docs/deployment.mdnow records that the production host deploys and runs the application as oneveetbotaccount, in place of the separateveetbot-deployidentity it describes, and lists reconciling that as an open operations item.
2026-09-03 — Operator-managed Gmail accounts¶
- Extended Milestone 18 under ADR-0085 so one Veetbot principal can use up to
eight operator-managed Gmail accounts concurrently. A versioned manifest
composes one separately credentialed read/write/send MCP triplet per account;
the default keeps the existing tool names and additional accounts are
explicit in names such as
mcp.gmail_work_read.search_threads. - Added account-tagged bootstrap credentials, fail-closed manifest validation, per-account credential and scope isolation, dynamic Gmail approval-floor enforcement, four registry-backed gates, and deployment/runbook guidance. Legacy single-account configuration remains unchanged.
2026-09-01 — Code blocks copy to the clipboard¶
- Added a labeled Copy action to every fenced and indented code block in the native Apple conversation renderer. Code snippets and shell or CLI blocks now copy their exact contents through the platform clipboard on iOS and macOS and display an accessible confirmation.
2026-09-01 — TestFlight installer signing isolated¶
- Replaced the unusable managed installer-signing bundle path with a dedicated,
project- and branch-restricted CircleCI context. Both the
devsigning smoke andmaindelivery import the password-protected installer identity into a fresh random-password keychain, grant headless signing access to that key only, verify the resulting package, and delete the decoded credential and keychain on exit. App signing remains in CircleCI's managedveetbot-app-storebundle, while App Store Connect upload authority remains isolated inveetbot-apple-testflight.
2026-09-01 — Milestone 22 specified: the persona surface¶
- Authorized and specified Milestone 22, the persona surface and curated
belief promotion, as a seventh parallel workstream (ADR-0079,
persona-surface.md). An owner-edited, versioned
persona document renders as a new trusted Region A prefix row directly
after the agent instructions, and a formed belief reaches that text only
through the owner's explicit affirmation of a governed nomination —
resolving the readiness review's last memory partial without weakening
"memory is data, never instructions". Fourteen
gate.persona.*entries open a new registry area; the census moves from 383 to 397 entries and the prefix ceiling from 15,000 to 17,000 tokens. Implementation has not begun; every Milestone 22 gate points at the pending check.
2026-08-29 — Model stream accepts ordinary risk-... URLs¶
- Boundary-anchored the normalized stream's
sk-provider-key detector so it no longer starts inside an ordinary word such as thesk-...substring of arisk-...URL. Genuine provider-key prefixes, authorization headers, bearer values, and private-key markers remain rejected. A production-derived model stream regression covers the URL false positive.
2026-08-26 — Routine fact lookups avoid sandbox approval¶
- Added least-powerful-tool guidance to the versioned default agent: routine
arithmetic, date/time questions, and public facts use available read-only
tools instead of falling back to
sandbox.run_command. When no read-only capability can answer, the agent explains the limitation or asks before proposing arbitrary code execution. The sandbox policy remains unchanged, so model-authored code still receives its normal deterministic review and one-time approval when proposed.
2026-08-25 — Milestone 17 completion recorded¶
- Reconciled the milestone record with the already merged delivery evidence.
Dev-to-main pull request 58 carried the memory read API and native Apple
browser to
mainwith the ten gates, Python and PostgreSQL partitions, native package and simulator verification, every hosted CI lane, GitGuardian, and the final CodeRabbit review green on its final head; every review conversation is resolved. Pull request 64 subsequently added a real PostgreSQL keyset walk with an interleaved write and fixture-driven iPhone and iPad list-to-detail coverage, again with all hosted lanes green and no unresolved review conversation. - Marked Milestone 17 complete without advancing the verified sequential gate
ceiling beyond Milestone 12, and moved its evidence from the in-progress
project state into verification history. The milestone's exclusions are
unchanged: recall-trace viewing still waits on tenant and principal
predicates for
PostgresTraceStore.for_turn; HTTP writes, consolidation and formation audit routes, and knowledge-document browsing remain deferred.
2026-08-25 — Reconciled milestones page and status refresh¶
- Added
docs/status/milestones.md, a grouped summary of every milestone — complete, in progress, authorized, and the deferred roadmap — with an open-item checklist for each in-progress milestone, and a top-level "Milestones" navigation entry. The page is a projection ofdocs/status/project-state.yaml: a newcheck_milestones_pagereconciliation inscripts/check_docs.pyfailsmake docs-checkon a wrong group, a stale title, a missing or duplicated entry, or a checklist that disagrees with the state file's new per-milestoneopen_itemslists. - Recorded Milestones 17 and 19 as in progress in the project state.
Milestone 17 already carried locally passing evidence for its ten gates;
Milestone 19's five registered gates pass locally through
tests/gates/test_schedule_tool_m19.py, recorded as new evidence. - Refreshed stale status prose to match: the documentation home and
project-state overview now point at the reconciled milestones page instead
of restating milestone progress,
docs/architecture.mdgained the Milestone 12 notification and device modules plus the landed Milestone 13, 16/17, and 19 additions, andAGENTS.mdandCLAUDE.mdcorrect their milestone and specification counts.
2026-08-25 — Milestone 13 capability scenario bound and hosted evidence¶
- Raised the capability scenario schema's closed milestone ceiling from 10 to 13, as the delegation design requires. Focused regression coverage admits a Milestone 13 trajectory-sourced scenario and rejects Milestone 14; it does not fabricate the real failed trajectory that the publication gate requires.
- Recorded the hosted evidence already completed by pull request 64. On final
head
071e6422503df592f8fe172484a7b1b304014540, all five CircleCI lanes, both Xcode Cloud archives, GitGuardian, and CodeRabbit passed; CodeRabbit had no final-head finding and every review thread was resolved before the pull request merged tomain. That evidence covers the implementation on that head, not this later local continuation. A real failed research trajectory is now exported under the correctedtrajectory@5privacy projection, reviewed, and admitted. Its first baseline attempt established that the USD 0.75 per-repeat cost ceiling is too low to reach the judge on the configured balanced model. The owner approved a USD 50 evaluation budget, now encoded as USD 5 per repeat, USD 25 per suite, and USD 50 per day; the two live scores and hosted verification of the completed evidence head were outstanding at that checkpoint. The scored baseline is recorded below; the delegating score and hosted verification remain outstanding. - Preserved the validator's fixed, content-free invariant name in
RunFailure.detailswhen a normalized model stream fails locally. The live research baseline exposed that the prior generic failure discarded the one diagnostic needed to distinguish a provider-stream defect from a governed credential-content rejection. - Refined bare bearer-value detection so ordinary documentation such as
Bearer authenticationremains model content, while raw authorization headers, provider-key prefixes, JWT-shaped bearer values, and long opaque bearer values remain rejected by the normalized-stream boundary. A second live false positive extended that accepted terminology to short mixed-shape phrases such asBearer token-based authorization. - Taught the live capability driver to distinguish a genuine pending approval
from Milestone 13's approval-free child-run suspension. A delegated research
subject now drives both interactive and asynchronous queue classes through
queued children and the resumed parent instead of misclassifying
WAITING_FOR_APPROVALas the subject's final state. - Made tool-free capability judges runnable without changing their zero-tool evaluation budget: the runtime-facing limit receives a one-call floor while no tools are registered and the harness still rejects any observed tool call. Live additive-usage evidence also raised only the research scenario's call ceilings from 40 model/60 tool calls to 80 model/200 tool calls; its USD 5 repeat, USD 25 suite, and USD 50 daily cost ceilings remain unchanged.
- Normalized one exact outer
jsoncode fence from a capability judge before applying the unchanged strict schema. Surrounding prose, nested fences, missing or duplicate criteria, extra fields, and out-of-range values remain invalid, and evaluation failures now report a content-free judge/schema diagnostic rather than echoing Pydantic's candidate-output excerpt. - Completed the scored, delegation-disabled five-repeat research baseline on
exact staged build
11e5b90: scores were 0.75, 0.55, 0.75, 0.65, and 0.75, with mean 0.69, floor 0.55, population variance 0.0064, zero ceiling hits, zero policy failures, and USD 7.855529 cost. The suite correctly remains release-blocked because the second repeat is below the rubric's 0.60 floor. The provider ledger reached USD 42.599510 of the approved USD 50 aggregate cap, leaving the scored delegating re-run outstanding rather than knowingly exceeding the approved limit. - Raised only the research capability evaluator's aggregate daily ceiling from USD 50 to USD 100 after explicit owner approval. The USD 5 repeat and USD 25 suite ceilings remain unchanged, and the provider ledger continues to count aborted attempts when enforcing the aggregate authorization operationally.
- Preserved the first scored delegating distribution rather than cherry-picking
it: exact build
877b3ecscored 0.75, 0.75, 0.775, 0.65, and 0.35 (mean 0.655, floor 0.35), with zero scenario ceiling hits, zero policy failures, and USD 10.984242 cost. Child traces tied the failure to exhausted default or caller-guessed child limits before a usable synthesis. - Calibrated the governed child defaults from 8/8/16 to 12/12/48
steps/model/tool calls, made each derived model/tool budget visible in the
child instructions with an explicit final-synthesis reserve, and revised
delegate.runto 1.0.1 so long-research callers know to omit optional limits unless they intend stricter caps. Exact repaired build3c8772a-delegating-repairthen scored 0.65, 0.65, 0.90, 0.65, and 0.75 (mean 0.72, floor 0.65, population variance 0.0096), with zero scenario ceiling hits, zero policy failures,release_blocked: false, and USD 11.104456 cost. That exceeds the recorded 0.69 single-agent baseline while keeping aggregate provider spend at USD 64.688208 of the USD 100 cap. - Closed the final-review gaps without discarding that live evidence: the
runtime now enforces explicit step, model-call, and cost headroom for a
delegated child's tool-free final synthesis;
delegate.run@1.0.0remains registered for exact persisted-session recovery alongside 1.0.1; and the normalized stream and trajectory exporter share one bearer-value rule that covers short opaque credential-shaped values without rejecting ordinary authorization prose.
2026-08-26 — Automated macOS TestFlight delivery¶
- Added a
main-onlyapple-testflightCircleCI job after the production API deploy. It installs CircleCI's managed Apple signing bundle, uses the project-scoped pipeline number asCFBundleVersion, verifies the archived bundle identifier, build number, and signature, and uploads directly to App Store Connect with a restricted API-key context. The key exists only in a mode-restricted temporary file and the signed archive is not retained. - Added ADR-0074 and documented the signing-bundle, context, TestFlight-group, automatic-update, counter, and first-delivery prerequisites. The Apple signing authority remains separate from the production-host deployment context, and the job reports upload acceptance rather than claiming Apple's later processing or device installation has completed.
2026-08-24 — Deployment sudo contract covers notifications¶
- Repaired the production sudoers contract so notification-enabled releases
can install, enable, restart, and verify
veetbot-notifywithout an interactive password prompt. The structural test now checks every supported base, schedule, notification, and combined unit list instead of checking only the base and schedule variants.
2026-08-24 — Milestone 18 specified: first-class email integration¶
- Added
docs/plan/email-integration.mdand proposed ADR-0071, meeting roadmap item B11's entry condition for its email half: owner intent, with email arriving as MCP servers. Three first-party stdio servers in one new package,src/gmail_mcp/, import-isolated fromagent_corein both directions and split by side effect because the tool system classifies at the server level —gmail_readatNETWORK_READ/LOW/READ_ONLY,gmail_writeatEXTERNAL_WRITE/MEDIUM/NON_IDEMPOTENT, andgmail_sendatEXTERNAL_MESSAGE/HIGH/NON_IDEMPOTENT— eight tools, no permanent deletion on any roster, every write and send approval-gated by the default matrix. Credentials are broker-heldenv-scheme references holding a refresh-token document only the server understands, obtained by a one-timepython -m gmail_mcp bootstrapconsent ceremony; composition is default-off behindAGENT_EMAIL_ENABLED; monitoring is a recipe over existing schedules and notifications. Thirteen hard gates in a newemailarea take the census from 335 to 348. - ADR-0071 also owns the one repair the milestone needs in shared code: the
deterministic
host_on_allowlistcondition, which today recognizes only the web and browser fixed provider targets and so denies every read-only MCP call, gains an arm for anmcptarget whose specification declaresNETWORK_READandREAD_ONLY, on the same operator-declared justification ADR-0054 recorded for the web arm. Calendar, attachments, Gmail push, interval recurrence, the email Surface, and the email notification transport stay on the roadmap. - Two boundaries the specification now states rather than implies, after
review. A stdio MCP server is a child of the worker and inherits its
network position, so neither the sandbox proxy nor the worker's outbound
guard confines it:
gmail_mcpis held to Google by its own endpoint constants, refused redirects, and gates 1 and 12, and host-level egress enforcement is named as a deployment precondition the milestone does not require. And because the two write servers areNON_IDEMPOTENTand Gmail can commit before answering, a rate limit, 5xx, or lost response after dispatch resolvesuncertainand is never re-proposed, rather than being retried into a duplicate label change or a second message — ADR-0071 decision 10, generalizing the rule the adapter already applies to a post-watermark 401. Reconciliation is explicit: a write is read back throughgmail_read, and a send goes to human review. The engineering plan's gate census sentence catches up with the registry at 351 declarations and 348 entries.
2026-08-23 — The provenance trio is exposed, not withheld¶
- The owner answered the open question the specification flagged for
sign-off:
formation_run_id,consolidation_policy_version, andorigin_scopesare exposed inMemoryViewrather than withheld (docs/status/questions-for-review.md, Milestone 17 section). The exposure list grows from twenty-three fields to twenty-six;tenant_id,principal_id,utility, andstore_positionremain withheld outright, and hard gate 9 asserts the new list exactly, in both directions, with its exact-match mechanism unchanged. The native Apple client's memory detail view gains a formation run, a consolidation policy version, and an origin scopes row in its Provenance section.
2026-08-23 — Milestone 17 specified: the memory read API and browser¶
- Added
docs/plan/memory-read-api-and-browser.mdand proposed ADR-0070: two read-only routes under/v1/memories, a keyset-paginated list and a detail route, both GET, both requiring the exact scopememory.readand mounted only whenAGENT_MEMORY_API_ENABLEDis set, which is off by default; a sensitivityceilingthat every request must supply and the server never infers, filtered strictly, with an above-ceiling belief indistinguishable from one that does not exist; status, belief-type, subject, source-session, and text filters, the text filter answered identically by both store adapters through the shared lexical helpers; aMemoryViewbuilt from an explicit exposure list that withholds tenant identity, retriever-internal utility, cursor internals, and — as a recommendation flagged for owner sign-off — three operator-tier provenance fields; and a browsing surface in the native Apple client that declares therestrictedceiling explicitly and degrades gracefully against a server without the flag. Ten hard gates. - ADR-0070 supersedes ADR-0045's closure of the HTTP route set for inspection
only, on the precedent ADR-0050 set, and is the authorized contract and
security review ADR-0049 requires before the server is expanded for the
native client. Writes over HTTP, recall-trace viewing, consolidation audit
routes, and knowledge documents stay out; trace viewing is additionally
blocked until
PostgresTraceStore.for_turngains tenant and principal predicates. - Registered the ten gates in the existing
memoryarea, which now spans four declaring specifications: the registry bound and declaring-spec census, the milestone-map area paragraph, per-spec table, gate-table digits (23 specs, 329 subject gates, 338 declarations, 335 entries), census row for Milestone 17 and its Decision 9 note, readiness verdict and section, current milestone, project state, AGENTS.md routing row and scope bullet, mkdocs nav, ADR index, a route-extension subsection in the API design, the review log, and this changelog. Raised the plan to version 2.6.
2026-08-23 — Milestone 16 complete¶
- Milestone 16, memory evaluation and lifecycle, completed as the parallel
workstream: all twenty
gate.memory.*gates pass, the checked-in benchmark baseline equals a fresh deterministic run exactly, the provider evidence is republished atformation@8, and hosted CI plus the CodeRabbit review loop finished clean on thedevtomainpull request. Its evidence moved from project state to the verification history. The verified gate ceiling stays at Milestone 12 and advances only in numerical order.
2026-08-23 — Live benchmark failures made diagnosable, and the arm published¶
- Made a failed probe arm say why it failed. Every arm of the live memory benchmark now records the terminal error class the runtime already carries — or the exception type when the arm failed outside the run — beside the terminal status, model calls, and cost it already recorded, and the command prints one content-free line per incomplete run beside its unchanged one-line summary. A failure landing outside the run no longer ends the whole invocation; it becomes a failed arm named by its exception type.
- Gave each probe arm one retry. An arm whose run terminates without an answer
is asked once more against a freshly built composition, admitted through the
same pre-admission cost check as any other run; an arm that completed is
never asked again, a second failure is kept, and a first attempt that failed
after a billed call still counts its cost. The artifact gains
retried_runsand an aggregatefailure_classeshistogram. No published pass condition changed:incomplete_runs == 0still means what it meant. - The live benchmark arm published its artifact. After two failed attempts,
the third run passed every condition: lift forty-five against a derived
minimum of eleven, recoverable correctness forty-five of forty-eight against
thirty-nine, abstention twelve of twelve against ten, no protected leak, no
policy regression, no ceiling hit, USD 1.144445 of a USD 4.00 ceiling, and
zero incomplete runs of a hundred and thirty-two. Four arms failed their
first attempt and completed on the retry — all four the without-memory arm of
the four
multi_hopprobes at positionp02, while the other twomulti_hopprobes and all sixty non-multi_hopwithout-memory arms completed first time. The failures are concentrated in the without-memory arm of fourmulti_hopprobes at positionp02; the cause is not diagnosed.
2026-08-23 — Provider memory evidence republished at formation@8¶
- Republished the bundled provider-memory activation artifact at
provider-assisted
formation@8from one live run of the 25-case corpus, and deleted the supersededformation@4artifact. The new artifact covers the exact tuple OpenAIgpt-5.6-sol, model policybalanced, policy profiledefault, soautoagain activates provider assistance for that tuple and falls back safely for every other one. The provider arm fully supported seventeen of twenty-one positive cases against the deterministic arm's nine, supported twenty labeled candidates against twelve, and both arms fabricated nothing and regressed no policy. The expanded threshold is the separately versioned Milestone 16 gategate.memory.provider_positive_coverage_v2; Milestone 10's completed sixteen-of-twenty gate and evidence remain unchanged. - Re-recorded the deterministic benchmark baseline as the milestone's closing act. The whole diff is the build reference and the recording timestamp: the aggregate metrics and all sixty-six probe rows are unchanged, so no metric moved.
- Ran the live benchmark arm twice, and neither run published an artifact. Both
measured what the arm exists to measure and passed every substantive
condition with margin — lift forty-five then forty-four against a derived
minimum of eleven, recoverable correctness forty-five then forty-four in
forty-eight, abstention correctness twelve in twelve, no protected leak, no
policy regression, USD 1.111295 then USD 1.128405 against a per-invocation
ceiling of USD 4.00 — and both failed the absolute incomplete-runs condition
with two failed runs in a hundred and thirty-two. The failures landed on
different probes and different arms each time, so the incompleteness is a
reproducible rate of about one and a half per cent rather than one transport
blip, and the arm records no terminal error class to name its cause.
gate.memory.bench_evidence_publishis satisfied by the unit-level publication proof and is unaffected; the live artifact is milestone evidence. The entry above records the harness change that answered this and the third run that published. - Recorded Milestone 16's verification evidence in project state, which stays
authorizedpending hosted CI on the final head and the CodeRabbit review loop on thedevtomainpull request.
2026-08-22 — Milestone 12 completed: notifications and device identity¶
- Added the principal-scoped device registry, durable content-free notification
outbox, transactional trigger producers, provider-partitioned dispatcher,
bounded retries, APNs HTTP/2 transport, least-privilege
notifyrole, seven exact-scope routes, offline inbox, and native Apple registration and deep-link restoration. - Closed review regressions for deterministic registration identity, per-target transport failure accounting, request-specific Apple idempotency keys, queued notification navigation, existing and changed focus scrolling, and exact APNs collapse-identifier wording.
- Passed all twenty notification and device gates, all 247 cumulative gates,
PostgreSQL integration and resilience, Apple package and iPhone/iPad UI lanes,
unsigned Release builds, hosted CI, and direct delivery to
dev. Code review is reserved for the final merge intomain. Physical APNs activation remains an explicit owner action outside the repository.
2026-08-22 — Milestone 16 specified: memory evaluation and lifecycle¶
- Added
docs/plan/memory-evaluation-and-lifecycle.mdand proposed ADR-0069: a checked-in multi-session benchmark corpus of labeled beliefs and probes across eight categories; a deterministic in-CI arm that drives real formation and real retrieval through the composition root under a fixed clock, a scripted model, and sequential identifiers; a checked-in baseline compared exactly, so a behavior change re-records it in the same change; an opt-in live arm pairing with-memory and without-memory runs, scored without a model judge, bounded by a pre-admission ceiling of USD 4.00, and publishing self-validating evidence that is never overwritten; opt-in loaders for LongMemEval, LoCoMo, and HaluMem read from a local path and never vendored; and the lifecycle the Milestone 9 memory specifications describe — memory profiles as configuration, operator-tier trace expiry, lexical parity between the store adapters, episode paging, project scope on the consolidation and query-former paths, time-decayed reinforcement with a forgetting sweep, usage feedback that never raises confidence, the recall delta and its correction lines, established facts at affirmed authority, surfaced conflicts, and opt-in re-derivation; twenty hard gates. - Registered the twenty gates in the existing
memoryarea, which now spans three declaring specifications: the registry bound and declaring-spec census, the milestone-map area paragraph, per-spec table, gate-table digits (22 specs, 319 subject gates, 328 declarations, 325 entries), census row for Milestone 16 and its Decision 9 note, readiness verdict, current milestone, project state, AGENTS.md routing row and scope bullet, mkdocs nav, ADR index, forward-looking notes in the two memory specifications, the evaluation harness, and the context engine, the review log, and the changelog. Narrowed roadmap item B6 to the residue this milestone does not take, and raised the plan to version 2.5.
2026-08-22 — Retryable and diagnosable memory formation¶
- Added exact ordinary-language formation for a newly started soprano-saxophone activity, prior tenor-saxophone experience, and recurring right-thumb pain as three separate provenance-linked memories; the symptom is sensitive and flagged for review.
- Preserved safe normalized provider failures, committed deterministic fallback immediately, and added durable retries after 60 seconds and 5 minutes without advancing the source watermark. Three failures emit a content-free exhaustion event and close the prefix.
- Added principal-scoped
agent memory diagnose --sessionand confirmed provenance-preservingagent memory replay --session --confirmcommands. - Accepted ADR-0068 and versioned the changed policies as deterministic
formation@5and provider-assistedformation@6. Historicalformation@4evidence remains inspectable but no longer activates the current tuple.
2026-08-22 — Documentation adopts Veetbot's visual system¶
- Replaced the stock indigo Material presentation with Veetbot's turquoise, orange, navy, and warm-paper palette; the rounded typography, soft brand gradient, cards, navigation states, tables, code surfaces, and responsive layout now follow the native Apple client's visual language.
- Added the bracket-face brand mark as the site logo and favicon and rebuilt the home page around the canonical plan, current milestone, and governed delivery evidence without changing documentation authority or publication behavior.
2026-08-21 — Milestone 15 specified: operational hardening¶
- Added
docs/plan/operational-hardening.mdand proposed ADR-0065: a declared, encrypted, off-host daily backup of the database, artifact store, and browser-profile ciphertext with a manifest and manual secret escrow; a restore rehearsal with a five-part verdict on the host, in CI, and by the owner from the off-host copy; a host health check on a closed signal list delivering deduplicated alerts through the Milestone 12 outbox with a dead-man's switch and an external uptime check for dead states; cloud and host firewall, SSH hardening, proxy rate limits, and a loopback-only structural gate; a code-only rollback that refuses to cross a schema boundary without an override and never downgrades; worker watchdogs; and sixteen hard gates. - Registered the sixteen gates in the new
opsarea; amended ADR-0046 with an amendments section; added forward-looking pointers on the deployment and security pages; every authorized milestone now has a design document and the census reports no zero row.
2026-08-21 — Milestone 14 specified: inbound surfaces and pairing¶
- Added
docs/plan/inbound-surfaces.mdand proposed ADR-0064: a Surface as a Milestone 12 device with an empty capability set; a Telegram long-polling transport in a least-privilegesurfacerole that holds only the bot token and the database credential; the ADR-0017 pairing ceremony given a home and an endpoint; the session-key resolver with its rotation rules; one ingress transaction per update with the receipt as the idempotency boundary and default-deny before any content is stored; a paired message as an ordinary run through the submission function the HTTP API uses; origin attributed on the write; notifications through the Milestone 12 outbox and replies through a separate surface-reply outbox, redacted and chunked; two scopes, six routes, two flags, and twenty-one hard gates. - Registered the twenty-one gates in the new
surfacearea; the registry grammar, declaring-spec census, milestone map, readiness review, current milestone, project state, HTTP and policy forward-looking subsections, the seam audit's "what Milestone 14 settles" note, the review log, and the changelog follow.
2026-08-21 — Milestone 13 specified: general-purpose subagents and delegation¶
- Added
docs/plan/subagents-and-delegation.mdand proposed ADR-0063:delegate.runas a suspending control tool with a structured brief; a dedicated child session and child run materialized in one transaction with intersected scopes, subset tools, derived limits, and a bounded deadline; theCHILD_RUNsuspension and a join that completes the parent's invocation once through arequeue_after_childedge in the single terminal writer; additive usage with cost reserved at materialization; depth and fan-out caps; adelegationsledger carrying the separate trace and artifact references; cancellation cascading downward only; a default-off flag; and the capability-scenario and case-32 evidence the plan's gate for multi-agent work requires before tenant activation. - Registered twenty-one Milestone 13 gates in the new
delegatearea; the registry grammar, declaring-spec census, milestone map, readiness review, current milestone, project state, and cross-document pointers in the tool system, run loop, evaluation harness, and policy designs follow.
2026-08-21 — Milestone 12 specified: notifications and device identity¶
- Added
docs/plan/notifications-and-devices.mdand proposed ADR-0062: aDeviceregistry with a client-minted installation identity and per-device muted kinds; a durable notification outbox written in the triggering transaction through a savepoint-wrapped hook on the single terminal writer and the scheduling accountant and materializer; exactly five trigger transitions; a content-free payload; claim-lease dispatch with staleness checks, a closed retry schedule, and token invalidation; an APNs adapter behind aPushTransportport; a least-privilegenotifyrole; three scopes and seven routes including an offline inbox; and the Apple client's registration and deep-link duties. - Registered twenty Milestone 12 gates in two new areas,
deviceandnotify; the registry grammar, declaring-spec census, milestone map, readiness review, current milestone, and project state follow. - Noted what Milestone 12 settles in the Section 29 seam audit and added the forward-looking route and scope subsections to the HTTP and policy designs.
2026-08-20 — Roadmap: Milestones 12 through 15 authorized and the Milestone 10 completion contract amended¶
- Added ADR-0061. Milestone 10 now completes on construction — all registered gates, the cumulative registry, hosted CI on the final head, and a clean final review — while tenant activation of self-authored skills stays evidence-gated and default-off as roadmap item B1; ADR-0057's provider extractor evidence already passed on the intended production model.
- Authorized Milestones 12 through 15 in order: notifications and device
identity (Apple push to the native client, the Section 29 device registry),
general-purpose subagents through
delegate.run(activation gated on evaluation evidence), inbound surfaces and pairing (first channel Telegram), and operational hardening (encrypted off-host backup, restore rehearsal, health alerts, firewall, rollback). Each begins with its detailed-design document and ADR, which declare its gates. - Added Section 21's Milestone 12–15 requirements and acceptance criteria and a "Roadmap beyond Milestone 15" subsection that ranks every remaining deferral with its entry condition, as Section 24 requires; Section 27.6 now places a child run in a dedicated child session always.
- Raised the gate-registry and documentation-check milestone bound from 11 to
15 as one
MAX_MILESTONEconstant; the milestone-map census reports a zero row for each authorized milestone until its specification declares gates. - Corrected stale figures (thirty-eight Milestone 10 gates, 204 and 227 cumulative), retitled the architecture and security pages past Milestone 3 with summaries through Milestone 11, amended ADR-0046's Caddy decision as superseded by ADR-0048, and updated the client page's route reference.
2026-08-20 — Inspectable formation evaluation and semantic provider claims¶
- Replaced free-form provider-authored memory statements with a closed semantic
claim schema, exact evidence quotes, and deterministic local rendering under
the new
formation@4andprovider-assisted-v2activation boundary. - Rechecked secret, injection, and transient markers against authoritative cited source text so provider paraphrasing cannot remove a formation safety signal.
- Made paired evaluation failures return per-case arm beliefs, consolidation and provider-audit counts, and fallback-versus-provider attribution while retaining the rule that only a passing run publishes activation evidence.
- Added symmetric fabrication reporting and a sixteen-of-twenty positive-case coverage floor, with a non-zero CLI exit for failed gates.
- Removed annotation siblings from JSON Schema references so OpenAI strict structured output accepts the semantic-claim schema, with recursive regression coverage for the provider request boundary.
- Normalized source-grounded specialized values from either semantic slot, rejected cue-only explanation styles, and retained deterministic metadata when an otherwise identical provider claim would weaken the fallback candidate.
- Labeled the explicit hiking hobby in the wife relationship evaluation case and
canonicalized irregular base verbs so
go hikingrenders asgoes hiking. - Rejected duration-only language-study values such as
three yearsinstead of allowing a grounded time phrase to become a fabricated language memory. - Accepted ADR-0057 after the version-bound OpenAI
gpt-5.6-solevaluation supported 18 of 20 positive cases and 19 candidates versus 9 deterministic candidates, with zero fabrication and zero policy failures in both arms, and bundled the passing schema-v2 evidence for exact-tuple startup activation.
2026-08-20 — Deployment provisioning contract hardening¶
- Split the browser control-plane credential to its own example path: the containerized profile service refuses secrets it does not own (uid 65532) while the agent units read the credential as the veetbot user, so the two variables can never share one 0600 file. A static test now rejects the shared-path configuration.
- Documented the browser-profile host prerequisites in the deployment guide — secrets, the read-only keyring, ownership and mode contracts, and the browser-origin TLS certificate — all discovered as undocumented hard requirements during the first production deploy of the browser tranche.
- Added
deploy/sudoers/veetbot-deploy, the committed non-interactive sudo contract for the CI deploy account, reconciled against the sudo commands in both deploy scripts by a static test so grant drift fails CI rather than the production release.
2026-08-20 — Public documentation site delivery¶
- Added an immutable, checksummed MkDocs artifact to the existing production
release and served it from
docs.veetbot.comthrough a dedicated static Nginx virtual host. - Made documentation promotion share the application deployment lock and Nginx rollback boundary, retained five releases, and verified the public release identity after deployment.
- Recorded DigitalOcean DNS and Let's Encrypt certificate provisioning as one-time operator prerequisites rather than storing infrastructure credentials in the repository or CircleCI.
2026-08-19 — Reading lanes, state-file split, and census reconciliation¶
- Added three reading lanes to the
AGENTS.mdoperating contract: the full order (lane A) stays the default, repairs guarded by an existing gate or regression test read a narrower set (lane B), and changes that cannot alter observable behavior read the minimum (lane C).scripts/architecture_checks.pyderives the minimum lane from the changed paths and rejects a declaration below it; the completion report now names the lane used. - Wired the lane floor into CircleCI: the static job runs
scripts/check_reading_lane.pyfirst, reading the newestReading-Lane:git trailer in the pushed range (absence means lane A) and failing when the declaration sits below the diff-derived minimum. The range's base ispipeline.git.base_revision, thenorigin/dev,origin/main, or the parent commit. - Split
docs/status/project-state.yamlso it carries status rather than history: completed-milestone evidence moved todocs/status/verification-history.yaml, and the ten-pass corpus-audit narrative moved verbatim todocs/status/corpus-audit-log.md. The state file's stale 172-entry census claims left with it. - Reconciled the milestone map's gate-table arithmetic paragraph against the
derived registry in
make docs-checkand corrected or de-counted the remaining stale census-bearing prose. Recorded in ADR-0060, which also retains the citation ledger over the earlier anchor proposal.
2026-08-19 — Milestone 11 local implementation complete¶
- Added the complete authenticated schedule lifecycle and occurrence HTTP surface with exact scopes, optimistic revisions, request idempotency, bounded pagination, immutable definitions, and default-off routing.
- Added fresh configured authority, finite definition ceilings, terminal run accounting and automatic pause, content-safe OpenTelemetry metrics, best-effort PostgreSQL wakeup, and offline recovery of completed, failed, cancelled, missed, and overlap-skipped results.
- Split interactive, asynchronous, and schedule production roles. The schedule role uses a least-privilege PostgreSQL composition and receives no execution or API credential; release tests cover both disabled and enabled activation.
- Replaced every provisional schedule registry target with executable coverage, bringing the registry to twenty-three Milestone 11 gates and 223 cumulative gates. Local static, contract, deployment, migration, PostgreSQL integration, and resilience lanes pass; hosted CI and final CodeRabbit review remain.
2026-08-19 — Automatic provider-memory rollout and evidence generation¶
- Replaced manual default-off provider-memory enablement with
auto,off, andrequiredmodes. Automatic selection uses matching operator or release-bundled evidence and otherwise retains deterministic formation with a content-free reason audit. - Added
agent eval memory-formation, a checked-in 24-case paired evaluation that derives model and corpus identity and atomically creates evidence only after positive lift, zero fabrication, and no policy regression. - Preserved the legacy enable flag as fail-closed
requiredmode and retained deterministic fallback for model-resolution and provider-call failures. - Bound the provider-evidence publication gate to its executable test method so the canonical cumulative runner collects and reports the check.
2026-08-19 — Milestone 11 persistence discipline¶
- Expanded the Milestone 11 contract from fifteen functional scheduling gates to twenty-three gates, adding explicit domain, adapter-contract, schema, migration, transaction, erasure, and isolation coverage.
- Added immutable schedule/revision/occurrence domain values, in-memory and PostgreSQL repositories, unit-of-work composition, four RLS-protected tables, and clean/stepwise migration coverage.
- Made erased materialized links an explicit audited state: session deletion
atomically clears the session/run links and records
links_erased_atwhile retaining non-secret occurrence history. - Passed the full 109-test PostgreSQL integration and resilience lane.
2026-08-19 — Milestone 11 civil-time recurrence¶
- Added validated one-time, daily, and weekly cadence values with aware-instant, IANA-zone, whole-second local-time, and normalized weekday boundaries.
- Added pure next and latest-due recurrence calculation from civil rules, including first-valid spring-gap resolution and earlier-instant fall-fold selection without iterating through downtime backlogs.
- Backed
gate.schedule.civil_timewith deterministic examples and generated coverage across UTC, one-hour DST, half-hour DST, and quarter-hour zones.
2026-08-19 — Milestone 11 scheduled-runs design¶
- Authorized scheduling as Milestone 11 rather than changing Milestone 10's established completion contract.
- Specified versioned schedules, immutable occurrence identity, deterministic civil time, bounded misfires, fresh authority at firing, atomic ordinary-run materialization, dedicated occurrence sessions, and separate schedule/run cancellation.
- Added twenty-three scheduling gates, including explicit domain, adapter- contract, schema, migration, transaction, erasure, and isolation coverage, and extended the canonical registry from 200 entries through Milestone 10 to 223 through Milestone 11.
2026-08-18 — Web output bounds and pinned-version compatibility¶
- Bounded
web.searchoutput to its declared 128 KiB tool limit by dropping trailing results before rendering; one schema-bounded result always fits inline. - Registered
skill.load1.0.0 as a compatibility version so sessions pinned before the 1.1.0 revision keep the capability, following thememory.rememberprecedent.
2026-08-18 — Web tool selection with an empty skill catalog¶
- Dropped
skill.loadfrom a context plan when the pinned skill catalog is empty, keeping the confined background-review child's exact allowlist, and advanced the context builder version tocontext-builder@3so persisted plans rebuild under the new rule. - Revised
skill.loadto 1.1.0: the model-facing contract demands an exact catalog name, a miss returns the stabletool.skill.not_in_catalogreason with the visible catalog, and invalid-argument and not-found tool failures now advisemodify_argumentsremediation.
2026-08-18 — Configurable CLI run wait¶
- Added
agent run --wait-timeout <seconds>with a positive-value boundary and a 300-second default, while preserving the durable run identifier and exit code 5 when the local wait expires. - Kept the setting local to CLI submission; the asynchronous HTTP API and native Apple client behavior are unchanged.
2026-08-18 — Provider-neutral public-web access¶
- Added stable
web.searchandweb.fetchtools with Tavily and Firecrawl adapters behind one provider-neutral port. - Selected capability-level routing, recommending Tavily for discovery and Firecrawl for clean page extraction while allowing either provider for either tool.
- Kept web access default-off, brokered provider credentials at call time, bounded provider responses, rejected non-public fetch targets, and preserved external-untrusted provenance through the complete tool pipeline.
2026-08-18 — Inspectable and provider-assisted memory formation¶
- Completed the governed CLI surface with belief
get, source-session filtering, formation-audit listing, and principal-scoped raw retrieval-trace inspection alongside the existing list, edit, and delete operations. - Added in-memory contract, CLI boundary, and PostgreSQL integration coverage for ordering, principal isolation, source-session filtering, consolidation counts, and provenance identifiers.
- Documented an end-to-end debugging workflow without expanding the closed HTTP route set.
- Added an evaluation-gated provider extractor for ordinary-language memory proposals. Its restricted maintenance call is schema constrained, tool-free, independently budgeted and audited, and always passes proposals through the existing source and safety gates with deterministic fallback.
- Added a version-bound activation-evidence schema and an explicit non-production
evaluation composition mode. Provider-assisted records use
formation@3while the deterministic default remainsformation@2.
2026-08-17 — Complete historical transcripts in the Apple client¶
- Added a principal-scoped, paginated session-message read that exposes only durable user and completed-assistant messages from the authoritative event log.
- Restored every historical turn before the Apple client attaches to the latest run, with persisted-sequence deduplication so SSE replay cannot duplicate the final turn.
- Covered pagination, transient read retry, malformed cursors, scope and principal isolation, and the relaunch regression, and proposed ADR-0053 for the seventeenth-route extension.
2026-08-16 — Explicit memory writes after recall¶
- Preserved the runtime's
MEMORYprovenance throughmemory.rememberinstead of replacing it with the default untrusted argument label. Explicit writes now succeed after a memory snapshot or recall even when the model normalizes the user's wording, while external and knowledge-derived turns remain rejected. - Added focused and PostgreSQL-backed regressions for the complete recall-then-remember path, including short normalized statements.
2026-08-15 — Documentation-derived regression coverage¶
- Made red-green-refactor evidence part of the repository operating contract, including a prohibition on weakening failing tests and boundary-level coverage requirements for public behavior.
- Added a required full-Xcode Apple test partition to local tooling and hosted CI. Release packaging now waits for native tests instead of accepting a Command Line Tools build that compiles Swift Testing bundles without running them.
- Added native client contract coverage for every typed HTTP operation, security-sensitive connection validation, missing credentials, conditional artifact reads, stable message retries, and unbounded loop-safe pagination.
- Added a PostgreSQL-backed conversation journey covering activity ordering, principal isolation, active-run deletion refusal, cancellation, deletion, tombstone idempotency, and converged history.
- Fixed future activity timestamps rendering as
in 0s, normalized seeded in-memory credentials, and removed the approval-list 20-page truncation while rejecting repeated cursors.
2026-08-14 — Cross-device conversation titles¶
- Moved generated conversation titles into the authoritative shared core so they survive a client reinstall or move to another machine.
- Derived titles for older null-title sessions from their immutable first user message, avoiding an event rewrite or a one-off data migration.
- Kept title assignment first-writer-wins, principal-scoped, whitespace- normalized, and capped at 64 characters across the API and terminal paths.
2026-08-14 — Authoritative conversation history and deletion¶
- Added a principal-scoped, paginated server session index with latest-run identity and activity ordering so client history is a mirror rather than a device-local source of truth.
- Made idempotent
Delete Everywherethe default destructive action. It rejects active work, purges the durable conversation graph, retains only a content-free ownership tombstone, and retries external artifact-byte deletion through maintenance. - Updated the Apple client to reconcile history on connect, foreground entry, and a periodic poll, and to remove local state only after authoritative deletion succeeds.
- Reported a specific server-upgrade error when a client reaches a deployment that predates the history routes, instead of surfacing the generic unsupported HTTP-request response.
- Added a post-promotion authenticated session-index probe so a server release cannot pass deployment verification while omitting the API used by the Apple client.
- Fenced in-flight and background reconciliation so it cannot restore a session after Delete Everywhere succeeds, and stopped polling while the app is not active.
- Hardened session cursors, kept activity timestamps monotonic, aligned the deterministic deletion and run-index adapters with PostgreSQL, and made connection settings report the result of the current save attempt.
- Removed the Apple history page cap and rejected cursor loops, tightened release-probe URL and status validation, and gave memory origin-trust rejections their own stable tool reason code.
- Required initial history compatibility and authentication checks to succeed before connection settings report success or dismiss their form.
- Made initial reconciliation fail closed for every error, streamed server pages into the local history cache, and bounded parallel verification of locally missing sessions.
- Removed a server-confirmed deletion from the visible Apple history immediately even when persistent device-cache cleanup subsequently reports an error.
- Proposed ADR-0050 for the two-route post-Milestone 9 extension and its deletion, synchronization, and artifact-lifecycle boundaries.
2026-08-10 — Atomic production delivery¶
- Adapted Mankunku's timestamped immutable-release, deployment-lock, exact
public revision check, bounded retention, and safe Nginx reload patterns to
Veetbot's
uv, Alembic, systemd, PostgreSQL, and gVisor topology. - Added post-gate CircleCI packaging and
maindeployment jobs while preserving the existing verification partitions and single configuration file. - Added
X-Veetbot-Releaseto both health responses, production checks for the release identity and default provider credential, and isolated shell tests for application and proxy deployments. - Expanded committed-secret scanning to the deployment and CI surfaces and proposed ADR-0048 for the privileged delivery boundary.
- Bound the production API to loopback behind Nginx, required a dedicated Veetbot deploy key, serialized proxy changes with application releases, and refused stale concurrent-pipeline promotions.
- Stripped terminal control sequences from remote client output and kept interactive session commands recoverable after API failures.
2026-08-10 — Downloadable API client¶
- Added a dependency-free terminal client that creates and resumes sessions, submits messages idempotently, reconnects SSE from the last persisted event, reconciles durable final messages, and handles approvals and user questions.
- Added an executable zipapp build and published the artifact from the existing CircleCI static job.
- Refused remote bearer tokens over plain HTTP, disabled redirects, retained default TLS verification, and kept tokens memory-only.
- Proposed ADR-0047 for the separate transport-only client and documented the initial terminal scope and deferred GUI, native-binary, and credential-store decisions.
- Corrected the production composition default from the deterministic fake
policy to the declared
balancedprovider policy; development and tests keep the fake default, and explicit policy selections remain authoritative.
2026-08-10 — Minimal single-Droplet launch path¶
- Simplified the initial topology to one Droplet with local PostgreSQL and no load balancer, cloud-firewall requirement, monitoring requirement, backup requirement, or high-availability layer; the runbook records the accepted exposure and data-loss risks explicitly.
- Made the runbook safe to apply on a shared Droplet: inventory existing
listeners and containers, reuse Docker and the active reverse proxy, choose a
free loopback PostgreSQL port, preserve Docker daemon configuration, and
verify existing applications after the
runscregistration restart. - Corrected the production environment's initial scope grant to contain only names from the executable closed platform vocabulary and added a regression assertion over the template.
- Disabled PostgreSQL TLS for the loopback-only production connection so asyncpg does not probe a service account home hidden by systemd hardening.
2026-08-07 — DigitalOcean production deployment assets¶
- Added the first production topology decision and operator runbook for a host-native DigitalOcean Droplet deployment.
- Added protected environment, systemd, Caddy, gVisor, and preflight assets for the API, worker, and maintenance processes.
- Kept account-, network-, host-, restore-, and smoke-test checklist entries open until the actual production server supplies evidence.
2026-08-07 — Sandbox approval-resume resilience¶
- Added a durable regression proving that an approved sandbox call resumes on a different worker composition with a fresh lease-scoped workspace.
- Normalized execution-service unavailability as a retryable transport outcome instead of an opaque, non-retryable internal tool failure, matching the sandbox isolation contract.
- Added an ignore rule for the untracked local
.envdeployment file.
2026-08-07 — OpenAI live-run compatibility¶
- Made
VEETBOT_OPENAI_KEYthe canonical OpenAI credential, withOPENAI_API_KEYretained as a compatibility fallback. - Added deterministic OpenAI-safe wire aliases for canonical dotted tool names and restored canonical names before normalized tool calls leave the adapter.
- Disabled OpenAI provider-strict function schemas while retaining centralized
JSON Schema validation, and omitted the unsupported
temperatureparameter from Responses requests. - Verified the exact documented
agent run --model-policy balancedcommand against the live GPT-5.6 Sol Responses API through the durable worker.
2026-08-04 — Milestone 9 long-term memory and knowledge retrieval completed¶
- Merged the reviewed Milestone 8 pull request after all hosted CircleCI lanes passed and every CodeRabbit thread was resolved.
- Authorized implementation of long-term memory formation and retrieval, user-managed beliefs, and knowledge-document ingestion and passage retrieval. The 26 Milestone 9 gates bring the cumulative active gate count to 166.
- Added provenance-bound explicit formation and deterministic consolidation, correction tombstones, reinforcement and supersession, human CLI management, expiry maintenance, hybrid structured/full-text recall, reciprocal-rank fusion, frozen session snapshots, in-turn recall, episodic search, and sensitivity-filtered faithful traces.
- Added retained knowledge-source artifacts, normalized deterministic chunking, versioned document ingestion, principal/project/tenant visibility, bounded verbatim passage search with citations, and deletion that cascades through chunks, bytes, and historical trace visibility.
- Added the five memory and knowledge builtins to the centralized policy and approval pipeline. Model-context provenance is checkpointed so snapshot and in-turn memory cannot silently authorize a subsequent persistent write.
- Added the Milestone 9 PostgreSQL migration and repository adapters, twelve new
port-contract suites, four adversarial/retrieval corpora, and deterministic
carry: [memory]evaluation with fresh source provenance in its isolated second arm. - Proposed ADR-0045 for the reversible implementation seams. Passed all 526 non-live tests across the static, contract, PostgreSQL, resilience, and real Docker partitions, all 166 cumulative gates, the strict documentation build, and the clean Alembic metadata check.
- Addressed all hosted CodeRabbit findings, including atomic session-close
consolidation, and resolved every review thread. The final incremental review
completed without a new finding. All four CircleCI lanes and GitGuardian
passed on pull request 10 commit
e5c2140.
2026-08-04 — Milestone 8 skills and MCP integration completed¶
- Added validated immutable skill packages, content-addressed deterministic archives, positive revision pinning, a capped metadata-only session catalog, and selective body loading with checkpoint and process-reconstruction support.
- Added official-SDK stdio and HTTP MCP adapters behind repository-owned ports, normalized discovery, bounded authentication recovery, dynamic tool and prompt registration, and ordinary centralized-pipeline execution.
- Added a managed audited worker egress proxy for HTTP MCP and exact constructed stdio child environments. Persisted destinations are revalidated against the current deployment policy whenever a composition is reconstructed.
- Added deterministic two-arm skill evaluation, scripted MCP round-trip and disconnect cases, and the no-socket MCP harness gate. Activated all 17 Milestone 8 gates, bringing the cumulative active gate count to 140.
- Proposed ADR-0044 for the reversible archive, pin reconstruction, prompt, official-SDK, child-environment, proxy, evaluation, and trace choices forced by the first implementation.
- Passed
make check, all 467 non-live tests against PostgreSQL 16 and Docker, the 74-test PostgreSQL integration partition, strict documentation builds, and all 140 cumulative gates. - Passed all four hosted CircleCI lanes on pull request 9. Addressed and resolved
all 19 initial CodeRabbit review threads and all three incremental findings;
the final hosted review completed without findings. Milestone 9 remains gated
on merging the reviewed Milestone 8 pull request into
dev.
2026-08-03 — Milestone 3 completed¶
- Authorized Milestone 3 and began implementation of the normalized model gateway, OpenAI Responses, Anthropic Messages, and OpenAI-compatible chat-completions adapters.
- Activated the work order for 15 new hard gates covering SDK isolation, identical provider contracts, stream invariants, tool-call identity, secret exclusion, malformed arguments, pinned resume, cost failure, Ollama, provider metadata, profile validation, redacted export, consent, and trajectory-sourced evaluations.
- Installed the official OpenAI developer-documentation MCP server for future sessions; this session uses the documented official-source fallback.
- Added strict declarative provider profiles, collision-free aliases, capability ceilings, immutable registry hashes, durable model pins, normalized streams, exact Decimal price snapshots, failed-call accounting, and bounded provider metadata with persistence and span mappings as its only readers.
- Added OpenAI Responses, Anthropic Messages, OpenAI-compatible chat completions/Ollama, recorded-stream, unavailable-credential, and upgraded fake adapters behind one contract suite. Provider SDK imports remain adapter-only, malformed tool arguments fail identically through every runtime path, and provider-only reasoning continuation stays in checkpoints.
- Added consent records, prospective run stamps, a fail-closed redaction and verification pipeline, a narrow content-addressed trajectory artifact store, 30-day expiry and withdrawal sweeps, CLI grant/withdraw/export commands, and conversion of already-redacted artifacts into trajectory-sourced eval cases.
- Added the twelfth deterministic evaluation case and activated all 15 Milestone 3 hard gates, bringing the cumulative active gate count to 72.
- Proposed ADR-0039 for the reversible provider, configuration, pinning, metadata, fixture, and narrow artifact-store choices forced by the first Milestone 3 implementation.
- Passed Ruff, strict mypy, 145 static tests, 57 contracts, all 255 non-live tests against PostgreSQL 16, both credentialed vendor smoke tests, strict documentation and citation checks, and all 72 cumulative hard gates.
- Addressed the complete CodeRabbit review and its incremental follow-ups
across the three stacked pull requests. CircleCI jobs 67, 68, and 69 passed
the static, integration, and contract partitions for pull request 4 commit
6768e82, closing the final acceptance gap.
2026-08-03 — Milestone 2 completed¶
- Authorized Milestone 2 and advanced the current milestone to PostgreSQL persistence and the durable worker.
- Began implementation against the 16 new gates for event sequencing, projections, upcasting, migration round trips, checkpoint recovery, fenced claiming, resume idempotency, transaction hygiene, and ORM confinement.
- Added the linear durable-runtime migration, confined SQLAlchemy rows and
hand-written mappers, PostgreSQL repositories and short units of work,
atomic per-session event sequencing, version upcasting, watermarked session
history, trajectory scaffolding, referenced full/delta checkpoints, usage
records, the fenced
SKIP LOCKEDqueue, and worker/maintenance process roles. - Refactored the shared session, run, executor, budget, and tool paths so state changes and events commit together while provider and tool I/O occurs outside transactions. Tool recovery now snapshots idempotency classification, uses an effect watermark, and returns the exact persisted result on deduplication.
- Activated all 16 Milestone 2 gates (57 cumulative) and added PostgreSQL race, rollback, projection rebuild, checkpoint deletion, crash/reclaim, stale-fence, migration, concurrent-tool, and fourteen-boundary recovery cases.
- Proposed ADR-0038 with the durable seam and schema decisions that require owner review.
- Passed Ruff, strict mypy, 80 static tests, 29 contracts, all 138 non-live tests, the clean Alembic metadata check, 123 citation checks, strict documentation builds, and all 57 active gates. A separate worker process completed the calculator flow and a repeated idempotency key returned the same committed run.
- CircleCI jobs 19, 20, and 21 passed the PostgreSQL integration, contract, and
static partitions for
devcommit75964a0, closing the final acceptance gap.
2026-08-01 — Milestone 1 completed¶
- Implemented the provider-neutral domain, ports, state machine, five in-memory repositories, scripted fake model, minimal context builder, inline dispatcher and runtime, and the shared session/run services.
- Added the validated tool registry and execution pipeline plus the bounded Decimal calculator and injected-clock current-time builtins.
- Added the declarative evaluation schema, authored model fixtures, eleven
initial cases, CLI
agent runandagent eval run, and shared contract suites for every declared port. - Activated all 28 Milestone 1 hard gates. The 41 cumulative gates, 78 static tests, 17 contracts, all 107 non-live tests, strict typing/linting, and documentation checks pass locally.
- Verified the required calculator CLI flow: stdout is exactly
391plus a newline and stderr contains the six progress lines in the specified order. - CircleCI's static, contract, and integration jobs passed for
devcommit30664ed, closing the final completion gap. - Proposed ADR-0037 and added review notes for the reversible decisions at the in-memory/durable, pre-policy, fixture-shape, eval-loading, and cross-process CLI seams.
2026-07-31 — Milestone 0 completed¶
- CircleCI pipeline 3 passed its hosted static, contract, and integration jobs
for
devcommit5b60bca, closing the last recorded Milestone 0 gap. - Project state now records the repository and engineering foundation as complete. Milestone 1 remains authorized but has not started.
2026-07-31 — Milestone 0 defaults and CircleCI¶
- Replaced the GitHub Actions workflow with a CircleCI 2.1 configuration that retains the static, contract, integration, and live partitions. ADR-0035 records the provider change.
- Expanded the six overlayable default documents and the frozen hardline rules. The executable configuration inventory now resolves exactly 106 unique, non-null knob paths. ADR-0036 records the five initial operational defaults the corpus named but did not value.
- Merged every change from
origin/docs/plan-completionbefore applying the implementation updates.
2026-07-31 — Milestone 0 implementation started¶
- Established the Python 3.12
uv/Hatch repository,agentCLI entry point, typed settings, structured logging, configuration assets, and documented security baseline. - Added the single-service PostgreSQL Compose stack, an empty Alembic revision, the pinned expected revision, and the four CI partitions.
- Materialized the 172-entry gate registry and made all 13 Milestone 0 gates executable, including import boundaries, secret scanning, transaction hygiene, migration-graph validation, and the six registry invariants.
- Passed
make install,make check, Docker Compose startup, and an empty-database migration against the Compose PostgreSQL 16 service. The live check exposed and corrected a Compose-version portability bug in the Makefile's health poll. Milestone 0 remains in progress because hosted CI has not yet supplied its own execution evidence.
2026-07-31 — The ledger now fingerprints the whole span¶
- A cited range was only ever checked on its first line.
excerptis one line because a human reads it in a diff, and it was also doing duty as the integrity check, sofile.md:10-20went on matching while lines 11 through 20 were rewritten underneath it. Each ledger entry now carries adigestover every line of the cited span. A change anywhere inside a range is drift and is reported as drift. - Relocation now has to prove the whole span moved. A first-line match only
proposes a candidate; the candidate survives when a span of the same width
starting there is in bounds and digests identically. That stops
--updatefrom repointing a range onto text whose trailing lines differ, and more than one survivor is still reported rather than guessed. - Fixed the legacy path the digest change had broken.
find_spanrequired digest equality, but an entry written before digests have an empty one while the file yields a real one, so no candidate survived and a pre-digest entry became permanently unrelocatable. Such an entry now relocates on an in-bounds excerpt match, and the digest is computed and stored at the location it moved to, so it acquires one instead of carrying an empty digest through every future relocation. An entry that has a digest must still match it exactly. - Rule 6's runtime half is no longer claimed as covered by the contract
suite. That suite asserts stream behaviour and cancellation, and two
adapters can agree on both while passing a provider object through a
parameter annotated
ModelRequest,ResolvedModel, orModelAttempt; behavioural agreement is not type hygiene. ADR-0001 now requires adapter-boundary validation or conversion, with a negative contract case per SDK adapter. - No product implementation was performed.
2026-07-31 — Twenty-one more findings, and a trust rule that did not hold¶
CodeRabbit's re-review of the two preceding commits, all twenty-one posted as inline threads this time. All twenty-one were real.
- Argument trust was inferred from a value match, and that is unsound.
argument_trustwas raised toUSERon a verbatim sixteen-character match againstUSER-labelled context. Equality shows two values are equal, not that one came from the other, so untrusted content could quote a string visible in the same request and be labelled trusted for it. The stated rationale had the direction backwards too — raising the label removes an approval rather than costing one. Provenance is now carried from where the call is constructed, the lower label stands wherever an untrusted item contributed, and unknown provenance staysEXTERNAL_UNTRUSTED. Corrected in ADR-0021 and in tool-system.md, which had the same rule. - Device revocation no longer waits for a run to finish. Section 29.7 requires revocation to be immediate and server-side; the stamped scope set let an in-flight run keep acting under withdrawn scopes. Device-channel actions now revalidate presence and granted scopes, with the stamped set as a ceiling revalidation can only narrow.
run.fencedis stated as non-terminal. A stale worker's append can land above the new owner's events, so a consumer deriving run lifecycle from the log must read it as diagnostic. A compare-and-set guard was rejected: it would make the record unwritable in exactly the case it documents.- Three more in
scripts/check_citations.py. The malformed-reference patterns missedLINE 3,README.md 3, andfoo_bar.md 3; they are now case-insensitive and accept underscores. A span is validated before use, because:0indexed the last line of the file through Python's negative indexing and a reversed range like:10-2produced an empty excerpt that--updatewould have adopted as truth. - Precision fixes: exactly-once narrowed to the state transition it can
actually guarantee;
WAITING_FOR_USER's resume path stated, since the index would otherwise deadlock every question the agent asks;expected_revisiondemoted from idempotency key to concurrency precondition;policy_version's truncated hash demoted to a display label with full digests recorded; the signature gate no longer rejectsstr; the milestone map's seven gates reconciled with its own claim to own no requirement; the evaluation suite drains post-run hooks before truncating; the ingest secret-scan gate no longer deletes the caller's input artifact;make checkgaineddocsso the CI union claim holds; the loopback fixture became function-scoped, since markers are per-test; the context engine's history boundary and yield order stated in one unit and one order; and the calculator's"391"and ADR-0033's route count corrected. - No product implementation was performed.
2026-07-31 — Twenty review findings, eighteen of them real¶
A second pass over CodeRabbit's review of pull request #1, which had reported one finding as an inline thread and nineteen more inside the review body.
- The citation checker missed three classes of input.
BAREandLOOSErequired two digits, so a prohibitedline 9passed unseen; both now match one or more. The end-of-file check tested only a citation's first line, sofile.md:10-999passed whenever line 10 existed; it now tests the span's end, through aspan_ofhelper shared withkey. And the ledger writer escaped quotation marks by hand, which produces invalid YAML for any excerpt containing\dor\s; both scalars now go throughyaml.safe_dump. - Corrected the Pandoc version in the preceding entry.
--syntax-highlightingarrived in Pandoc 3.8, not 3.10, and--highlight-stylewas deprecated in that same release. The gate inscripts/build_docs.pymoved from 3.10 to 3.8, so Pandoc 3.8 and 3.9 now take the new spelling instead of emitting the deprecation warning the change existed to remove. - Aligned the Python version with the specification.
development-toolchain.md and ADR-0025 both
pin
requires-python >=3.12and one CI version with no matrix, whilepyproject.tomldeclared>=3.11, the README said 3.11+, and the workflow pinned nothing at all. The implementation now follows the specification rather than the reverse. - Fixed six counts and identifiers that an audit would trip over: the type
count in Section 18 (seven named, not eight), the four-versus-six enums in
policy-and-approvals.md's build sequence, ADR-0004's five columns that were
four columns and an index, ADR-0030's prose line reference, ADR-0033's
"eleventh gate" now named
gate.knowledge.no_belief_from_document, and the eight-fields-versus-ten-names ambiguity in.env.example. - Closed four contract gaps:
AuthoringContextis defined rather than only referenced;knowledge.ingesttakes a title in both descriptions;ModelRequestStarted.prefix_sha256isNOT NULL, because a null cannot participate in the exact-one-hash stability gate; and the sandbox production-startup gate is parameterized over every mechanism outside{microvm, gvisor}instead of namingdockerand silently exemptingfake. - Dropped the events index that served nothing. The migration added
(session_id, id)for "watermark scans", but a projection scanssession_id = ? AND sequence > watermark, which Section 15'sUNIQUE(session_id, sequence)already serves. - Two findings were rejected with cause.
agent run exportis not an undefined command:event-log-and-persistence.md:557-558gives its arguments and output, and bootstrap-and-composition.md deliberately keeps it out of "the twelve" as a subcommand of an existing command. And the supersededModelProviderandRunRepositoryblocks in the plan are already labelled as superseded in the prose beneath them, and are retained on purpose as the record of what they replaced. - No product implementation was performed.
2026-07-31 — The append path's three wrong sentences¶
- Required the append transaction to roll back when its guarded
UPDATE runsaffects zero rows, in event-log-and-persistence.md. The document had claimed that a writer losing the status-and-lease race "commits nothing at all", which is not what the SQL does: a zero-rowUPDATEis not an error and aborts nothing, so the sequence allocation and the eventINSERTwould have committed without the state change — producing therun.completed-while-RUNNINGrecord the same paragraph forbids. An append carrying no state change, the diagnosticrun.fenced, is named as the explicit exception. - Corrected the lock explanation behind the pinned allocation mechanism. PostgreSQL holds a row lock until the transaction ends, not until the end of the statement that took it, so the claim that the atomic increment "holds it for one statement rather than for the caller's whole transaction" was false and the stated reason for pinning it did not hold. The pin stands on the correct reason: it is one atomic read-modify-write with no application round-trip inside the lock.
- Re-founded the silent-missing-write defence on the mechanism that actually
provides it. The document had credited the partial unique index, but
UNIQUE INDEX (session_id) WHERE status NOT IN (...)constrains therunstable to one non-terminal run per session and says nothing about who appends events — and a session already has two appenders, since the submit handler appends the user message from its own transaction (http-api-and-streaming.md) while a worker appends run events. The guarantee comes from every writer allocating its sequence inside the transaction that inserts the event, which the corrected lock lifetime makes serializing. The two errors were connected: the false lock claim is what had made the multi-appender case look unsafe. - Widened hard gate 1 to append concurrently within one session and not only across sessions. Separate sessions share no sequence space, so the gate as written exercised none of the serialization it was there to protect, and the document's claim that a test asserted the defence was not yet true.
- Propagated the correction to ADR-0003, the failure table, and the document's closing summary, and added a failure-table row for an event committed without its state change. Revised the corresponding entry in questions-for-review.md and recorded the new rollback requirement beside it.
- Found by CodeRabbit on pull request #1; the third finding was re-derived rather than applied as written.
- No product implementation was performed.
2026-07-31 — Pandoc's renamed highlighting flag¶
- Selected the single-HTML build's syntax-highlighting flag by Pandoc
version in
scripts/build_docs.py. Pandoc 3.8 added--syntax-highlightingand deprecated--highlight-stylein the same release; Pandoc older than 3.8 does not accept the new spelling. Both take the same style name, so the build readspandoc --versionand passes the spelling that Pandoc understands, with the gate set at 3.8 because that is where the new option appears. - Chose the version gate over replacing the flag outright because the
repository fixes no Pandoc version and the two environments that build
the documentation disagree in practice. CI installs whatever
apt-get install pandocsupplies in docs.yml, and the README tells contributors to install it from their own package manager, so a contributor on a current release and a CI runner on a distribution package can differ by several minor versions. Switching the spelling outright would have silenced the warning on the newer Pandoc and broken the documentation build on the older one. - The style is unchanged:
pygmentsremains a valid style name under both spellings, and the generateddist/engineering-documentation.htmlcarries the same highlighting rules as before. - No product implementation was performed.
2026-07-31 — The values, audited¶
- Corrected
StopReason.STOPin runtime-loop.md toStopReason.END_TURN. The member does not exist: model-gateway.md declares seven andSTOPis not among them. It was the only dangling member reference among sixty five checked, and it names the empty-turn retry path, so the wrong value sat on the trigger condition forEmptyModelTurn. - Gave
SandboxMechanismits fourth value in the one file that enumerates them. bootstrap-and-composition.md'sSettingscomment listed three; six other places say four. The enum is declared nowhere, so the comment was authoritative by default andsandbox: fakewould not have parsed. - Extended the same file's startup check 4 to refuse
fakein production besidedocker, which sandbox-isolation.md's seventeenth requirement already asks for.fakeexecutes nothing, so a production deployment configured with it would have started and reported tool calls as run. The setting and its refusal are both Milestone 1. - Changed the trajectory export's
outcomein event-log-and-persistence.md fromSUCCEEDEDtoCOMPLETED.SUCCEEDEDis the tool-invocation spelling and exists nowhere at the run level. The export is persisted, versioned, and read by consumers who are not us, so a filter on the run vocabulary would have matched nothing. - Recorded
SuspensionKindas an observation rather than a change. It is never declared, its members live in trailing comments in two spellings, and the open hand-off question should produce the declaration. - The census behind all of it: twenty eight enum declarations, twenty
seven distinct, one benign duplicate with identical members, sixty
five dotted member references checked against their declarations, and
twenty two
Literal[...]annotations. Also a numeric-constants cross-check across retry counts, expiries, leases, and token budgets, which found no conflict.
2026-07-31 — The types, audited¶
- Added the supersession paragraph
model-gateway.md never wrote for
ProviderReasoningItem. The plan declares three fields, the gateway declares six, andopaque_payloadbecomesprovider_payloadwith no sentence anywhere saying so. The field is persisted throughRunCheckpoint.conversation, so the two declarations would have produced two different keys in the same stored JSON. The paragraph names all four differences, states that the plan's rules for provider-opaque items still govern, and leaves the plan's own field name to be settled as a question. - Added a pointer under the same document's
ModelCapabilitiesfence to the reconciliation table three hundred and fifty six lines below it. The reconciliation was already complete; nothing at the declaration said it existed. - Recorded the plan-side rename as that document's ninth open
question, beside the sixth, which asks the same thing about
tool_callingandvision. - The census behind all of it: one hundred and forty four distinct types across one hundred and fifty nine declarations, thirteen declared more than once, four of those with identical member sets, and eight of the remaining nine already labelled as extensions or supersessions.
2026-07-31 — The ports, audited¶
- Corrected runtime-loop.md, which said
"Four ports the runtime needs are named in the corpus and
declared nowhere" and then declared five. Four is the number of
code fences;
ClockandIdFactoryshare one. The same document declaresCancellationTokenunder cancellation, so it declares six ports in all. - Assigned the eight declared Protocols that
bootstrap-and-composition.md
left out of its ports table:
SkillRepository,SkillPackageStore,Extractor,Chunker,KnowledgeStore,WorkspaceHandle,ArtifactWriter, andCredentialResolver. That table exists so the first implementer does not invent a layout the second disagrees with, and the harness gates on a walk ofagent_core/ports/that demands a contract module per Protocol, so the omission was load-bearing. - Added
ports/knowledge.pyandports/credentials.py, with the reasoning for both, and added them to the layout additions. The other six went into existing modules under the document's own rule. - Pinned the census the table is now checkable against:
forty-seven
Protocolblocks naming forty-three distinct types, four blocks re-declaring an earlier type and four types being the application services of the API document, which leaves thirty-nine ports across fourteen modules. - Named the five retrieval ports and
ToolRegistryin the table, which had described them in prose while naming every other port individually. - Left three rows naming no type and recorded them as a question:
ports/telemetry.pywith no telemetry Protocol anywhere, the formation half of the memory row, which is prose bullets while the retrieval spec beside it declares five Protocols, and the MCP row.
2026-07-31 — The schema, audited¶
- Corrected both column counts in
http-api-and-streaming.md. The
paragraph introducing
GET /v1/runs/{run_id}said therunstable has fourteen columns and that the endpoint returns nine of them. Section 15 declares fifteen, four other documents add eleven more, and the JSON body immediately below has thirteen top-level keys. The overview of the same document called it twenty-three columns, which is Section 13's error-class count. - Stated the split rather than a count. Thirteen columns go out and
thirteen are withheld, and the line falls almost exactly between
Section 15 and the documents that extend it: every Section 15
column is in the body but
lease_ownerandlease_expires_at, and every later addition is withheld, thoughdeadline_atreaches the client insidelimits. Resolution row 12 and decision 22 record it. - Corrected readiness.md, which summarized the
Idempotency-Keyresolution as "two scopes, two tables, two milestones". The API document resolves it as one table and one column —idempotency_keysagainsttool_invocations.idempotency_key— and its own resolution row says two mechanisms and two scopes. - Added the missing cross-reference to
tool-system.md. It and
policy-and-approvals.md both add
origin_trustandidempotency_classtotool_invocations, bothNOT NULL. Onlyorigin_trustsaid so. - Recorded two open questions rather than closing them: whether the
memory, knowledge, and device stores should get DDL, since they
are the only storage in the corpus declared as Pydantic models
and prose, and whether any document should own a census of the
twenty-two tables the way
bootstrap-and-composition.mdowns the CLI census.
2026-07-31 — The route table, audited¶
- Added the missing scope-table row to
http-api-and-streaming.md. The
document specifies fourteen routes and the table had thirteen
rows; the one missing is
GET /v1/sessions/{id}, the route the document itself adds.session.readwas consequently a scope in the closed vocabulary that no route required. The document's own hard gate 5 walks the route table and fails the build on a route that declares no scope, so it declared a gate its own table would fail. - Corrected the overview, which said the document "adds nothing to
the API surface that Section 16 did not already put there" and
that "every route below is a route the corpus already names".
Its own specification of
GET /v1/sessions/{id}, its resolution row 8, and its decision 20 all say otherwise. - Stated the sum. Thirteen is what the document inherited — nine from Section 16, three named elsewhere, and the health probes that Section 16 counts as one — and fourteen is what it leaves. The heading now says "Thirteen inherited routes", the opening paragraph carries the sum, and decision 21 records it.
- Corrected five downstream sentences that closed the API at thirteen routes: readiness.md, skills.md, knowledge-documents.md three times, and engineering-plan.md twice, one of which asks for an error mapping for each of thirteen routes in the same paragraph that says one route is added.
- Corrected a CLI ordinal.
knowledge-documents.mdcalled the next command a fourteenth noun; the CLI is twelve commands and bootstrap-and-composition.md calls the next one a thirteenth. The CLI census is otherwise sound.
2026-07-31 — The tool registry, audited¶
- Removed
context.compactfrom the control-tool table in tool-system.md. It is a span name: runtime-loop.md nests it under the step span, the event compaction emits iscontext.compacted, and context-engine.md says compaction is a model call and therefore "not somethingbuild()does". The context engine's actual control tool iscontext.update_working_state, which now holds the row. The lead-in said "Three of the tool names" over four rows, so the set is still four and every sentence in skills.md that counts it is still correct. - Corrected two more live sentences in
tool-system.md that called
skill_managea control tool, against its own registration table and against skills.md, which argues at length that a tool which writes files cannot be one. An earlier pass fixed the table and the skills spec; these two are what a fix by search leaves behind. - The builtin roster is eight tools of eighteen.
builtin-tools.md now names the ten that
other specifications declare, scopes its classification table and
its registration steps to the eight it owns, and imports the rule
from knowledge-documents.md that
makes the count of eight correct. Eight of the eighteen declare no
ToolSpecfields anywhere, which registration step 6 reads; that is recorded as a conflict rather than closed. - Split the domain partition row that labelled
memoryandskillas control domains.memoryholds three capability tools and no control tool, andskillholds one of each. - Corrected readiness.md, which said the
agent-facing memory surface is two tools and that both read.
memory.rememberis a third and it writes. The gap the sentence supports is unchanged: none of the three lists, edits, or deletes.
2026-07-31 — The event catalogue and the error taxonomy, audited¶
- Read the event catalogue against the documents that declare into it.
Six declared types were outside the fifty-one that
questions-for-review.md recorded
as closed.
mcp.server.reauthenticatedsits in the same tool-system.md table as the seven the consolidation took, andknowledge.document.ingestedarrived with knowledge-documents.md. Both are session-scoped and both are now in the consolidated list in runtime-loop.md, which stands at fifty-three. - The remaining four are the
eval.*events, and they are a different problem. evaluation-harness.md puts them on the harness rather than on a run, under a span root that is explicitly notagent.run, so they have no session — andevents.session_idisNOT NULL. They are event types that cannot be rows ineventsas the schema stands. That is the wall multi-device-and-surfaces.md already names for device lifecycle events and leaves open. The two documents now point at each other, and one open question covers both rather than each inventing a table. - Corrected three event names in live prose that name nothing.
skills.md listed
session.openedandtool.invokedunder the claim that none of the three is new; bootstrap-and-composition.md told the CLI to render tool activity fromtool.invokedandtool.completed. The Section 6.8 names aresession.created,tool.call.started, andtool.call.completed. This is the same defect the harness had, and it was corrected there by the same rule. - Corrected policy-and-approvals.md, which said "One new event type" over a block of two. Its own decision 28 says two.
- The error taxonomy is twenty-nine classes, not thirty-one. The eight
that runtime-loop.md classifies include
BudgetExceededandConflictError, which Section 13 already lists and leaves unclassified, so only six are new. http-api-and-streaming.md inherited thirty-one and then invented "the two internal counterparts the loop resolves itself" so that subtracting four would leave the twenty-seven rows its table actually has. Set arithmetic gives exactly two absent,WorkerFencedandEmptyModelTurn, both already named in the same sentence. The phantom pair is gone and both counts follow from the lists.
2026-07-31 — The evaluation case registry, audited¶
- Read the case table against every statement that counts it. Eleven cases are writable in Milestone 1, cases 1 through 11, which development-toolchain.md, the engineering plan, and the harness's own build order all say. Three live statements said ten, and one said a Milestone 1 checkout fails twenty of the twenty-five when the figure is fourteen. All four are corrected, and each now names the range as well as the count so the number is checkable against the table rather than merely asserted.
- Corrected the heading "The twenty-five cases, with milestones and
gates", which headed a table with no gate column. It now names
the
Kindcolumn the table actually carries. - Recorded what that heading was pointing at. Ninety-five of the
hundred and seventy-two registered gates declare kind
case, against thirty-one enumerated cases, and six cases are tied to a named gate anywhere in the corpus. The enumeration is a floor rather than the finished suite, which evaluation-harness.md now says, and where the binding belongs is its seventh open question. - Ran the last two of the milestone map's own hard gates that can be
run today. Every identifier matches the grammar and its area is
one of the fourteen, with no area unused and no slug repeated
within one. The written census is exactly what the registry
derives, per milestone and cumulative. Only the set comparison
against
evals/gates/*.yamlremains, and it cannot run until that directory exists. - Left ADR-0022 alone. It carries the same stale figure and is a record at a point in time.
2026-07-31 — The map's own hard gates, audited by hand¶
- Ran hard gates 1, 3, and 4 of milestone-map.md by hand against the corpus, the three checks over the registry that the identifier audit did not cover. Gate 1 holds item by item: fifteen hard-gate sections, one hundred and seventy-three numbered items, exactly one milestone token each. Gate 3 holds per spec and not only in aggregate: every spec's gate count minus its declared aliases equals the entries citing it, and all three aliases name their owner and its gate number.
- Found that gate 4 cannot pass as written. It requires each
registry entry's
#hard-gatesanchor to exist in the built site, and the two entries the engineering plan owns have no such section to name — the plan is organized by milestone and declares them in Milestone 0's acceptance criteria. Resolved by the gate's own title, "Everyspecfield resolves", which is also how evaluation-harness.md states the rule: the two name the Milestone 0 heading that declares them, and that anchor is confirmed present in the built site. - Recorded the reading of gate 7. Its sentence asserts against the build-sequence table, which passes for all nine specs whose sequences carry milestones. The stricter per-gate reading is possible for only one table of fifteen and fails there on two rows the same document places deliberately and defends at length, so the sentence's reading is the one stated rather than left to whoever implements it.
- Added one conflict, one decision, and one open question to milestone-map.md, and three entries to questions-for-review.md. No gate statement is changed and the census is untouched: still one hundred and seventy-two registry entries.
2026-07-31 — Gate identifiers reconciled against the registry¶
- Widened the identifier column in four tables of
milestone-map.md and wrote all thirteen
truncated rows in full. The grammar that document sets admits no
dot inside a slug, so
gate.runtime.one_terminal_wr..is not an identifier at all, and two of the document's own hard gates fail on one: the grammar gate because it does not match, and the set comparison againstevals/gates/*.yamlbecause a truncated form cannot be a set member of anything. - Restored nine of the twelve affected gates from spellings already attested elsewhere in the corpus — eight from sandbox-isolation.md and one from both the event log and the runtime loop. The other three had never been spelled anywhere and are completed from their own declaration titles, which is the one new naming decision in this pass and is recorded as a question.
- Answered a question already on the record, which asked whether to widen the column or keep the thirty-character ceiling that had begun shaping identifiers. It named two truncations; the audit found twelve gates across thirteen rows.
- Corrected two worked examples in evaluation-harness.md that named gates the registry does not hold, and two milestone counts in one of them that disagreed with the census.
- Re-derived the census from the corrected tables and confirmed it unchanged: one hundred and seventy-two registry entries, the same four kind totals, and the same per-milestone counts.
2026-07-31 — A pass that tried to falsify the corpus¶
- Ran a closing pass whose only job was to falsify the claim that the corpus is complete and that no specification names an unresolved blocker. It raised ten findings. Checking each against the sources confirmed eight and cleared two: one is an explicit deferral, which readiness.md already rules is not a gap, and the other was already an open question in the document it was raised against.
- Reversed a verdict an earlier pass had recorded, which read http-api-and-streaming.md as the document in error about which milestone the approval routes land in. It is not the document in error. Milestone 5's implement list is the entire HTTP surface and Milestone 1 has no HTTP API at all, so no route can precede Milestone 5, and policy-and-approvals.md already says the CLI calls the application service rather than the route. Build step 11 in that document is narrowed to the service methods and the CLI, and a new contradictions row records the split.
- Registered the skill management tool as
skill.manage. Section 30.2 spells itskill_manage, which the name grammar attool-system.md:336rejects, because every registry name needs at least one dot and a capability tool is a registry entry. Theskilldomain already holdsskill.load, so no new domain is needed. The plan's spelling stays as the word for the tool; the string the registry, the policy rules, and the model see is the dotted one. - Corrected two cross references that pointed at the wrong line and the wrong section, one description of a sibling document that the sibling had since outgrown, and one sentence in milestone-map.md that still read as though Milestone 10 added no gates of its own, after skills.md gave it six.
- Gave Section 31 the outward pointer every expanded section carries, naming the two documents that expand its production half and the one that already held its consumption half.
- Recorded the one finding this pass does not fix. The
MemoryStoreport declareslist,edit, anddelete, and no tool, route, or command in the corpus calls any of them, so "A user can inspect and delete stored memories" is the one Milestone 9 acceptance criterion with nothing behind it to test. Designing edit semantics over an append-only belief store is Milestone 9 work, so the finding is sharpened in readiness.md and left open.
2026-07-31 — Milestone 10, answered by re-measuring it¶
- Answered the last open question in readiness.md, which asked whether Milestone 10 needs acceptance criteria or is correctly an open direction. It is correctly an open direction. Two of its four parts gate on evidence rather than on a date — a scheduler comes "only after durable on-demand runs are reliable" and subagents come "only when evaluation evidence shows that a single agent fails" — and acceptance criteria are a promise about a delivery, which cannot be made about work that must not start until evidence arrives. What the milestone is missing is the heading, not the content the heading would hold.
- Re-measured all twenty-two Milestone 10 requirements against the corpus before answering, because the answer turns on what is actually behind the milestone rather than on how it is formatted. The scheduling and routing verdicts held. The subagent verdict did not.
- Corrected this review's own subagent count, the only verdict in it
that later documents overtook. It named five of nine requirements
as having no design; five of the nine are supplied now. Restricted
context is
context-engine.md:278plus the child-run recall class atmemory-retrieval-and-ranking.md:87, the restricted tool set istool-system.md:949, and the child deadline isruntime-loop.md:1141, all written after the verdict was. Two are partial and two still have none: the separate trace and the artifact references, which no specification picks up. - Recorded one conflict the stale count was hiding. event-log-and-persistence.md enforces one active run per session with a unique partial index, and a parent suspended on a child is not in a terminal status, so a child run in the parent's own session cannot be inserted. Section 27.6 offers the parent's session or a dedicated child session per policy, and only the second survives the index. It is recorded rather than resolved, because resolving it is Milestone 10 work and this review authorizes none.
- Left the verdict and the gate census alone. Milestone 10 is still not a milestone, its six gates still come from skills.md rather than from criteria of its own, and no document was added, so the census stays a…10280 tokens truncated… container escape Section 28.7 has required since version 2.0 and the twenty-five-case table never carried. None of the twenty-five is renumbered, and the rule that no case is ever renumbered is now written down.
- Declared thirteen gates in a new twelfth registry area,
sandbox. Registry entries go from one hundred and five to one hundred and eighteen and declarations from one hundred and eight to one hundred and twenty-one. Milestone 1 goes from twenty-seven gates to twenty-eight, Milestone 4 from twelve to thirteen, and Milestone 6 from zero to eleven — Milestone 8 is now the only zero belonging to a milestone that does work. Case gates go from fifty-one to fifty-nine and structural from thirty-three to thirty-seven. The milestone map, the harness gate table, and the readiness review are updated; the ADRs are not, per the rule that an ADR's arithmetic records what was true when it was decided. - Updated the readiness review: Milestone 6 moves from not ready to ready, the four plan sections no specification expands become three, two of the four reported conflicts are marked resolved, and two of the five open questions are answered.
- Corrected two stale counts in the engineering plan's routing paragraphs that predate this work and had missed the secret scanner's registration.
2026-07-27 — The secret scanner joins the gate registry¶
- Registered the secret scanner as
gate.structure.no_committed_secretsat Milestone 0.bootstrap-and-composition.mdspecified it in full — five rule families, an allowlist that requires a reason, a report that never prints the match — and gave it a gate identifier, but the identifier's area wassecurity, which the identifier grammar inmilestone-map.mddoes not define, and no registry entry existed. A check that fails the build and sits outside the registry is the drift the registry exists to prevent, and it was there before any code was written. - Corrected the identifier's area to
structure. That area is defined as the structural statements about the repository that no single subject spec owns, which is what a repository-wide scan is. The milestone map already saidstructureheld three gates while only two existed; it now names all three. - Gave the gate the engineering plan as its owner rather than
bootstrap-and-composition.md, because the plan's Milestone 0 acceptance criteria already declare it in the same breath as the import-boundary walk.bootstrap-and-composition.mdstill declares no gates of its own; it supplies the mechanism, not the requirement. - Registry entries go from one hundred and four to one hundred and five and declarations from one hundred and seven to one hundred and eight. Milestone 0 goes from eleven gates to twelve and every cumulative figure after it rises by one. Structural gates go from thirty-two to thirty-three. The milestone map, the harness gate table, and the readiness review are updated; ADR-0027 and ADR-0028 are not, per the rule that an ADR's arithmetic is a record of what was true when it was decided.
- Corrected a stale count of my own: the milestone map's gate 5 required
a gate identifier's area to be one of the ten, which stopped being
true when the API specification added
apias an eleventh. The grammar block listed eleven and the gate that enforces it said ten.
2026-07-27 — The HTTP API, the event stream, and the error vocabulary¶
- Added
docs/plan/http-api-and-streaming.md, the specification the readiness review named as the single most valuable document not yet written. It expands Section 16 of the engineering plan, the three approval routes inpolicy-and-approvals.md, andPOST /v1/runs/{id}/inputfrom ADR-0009 into one contract: thirteen routes, each with a request shape, a response shape, a status code, a required scope, and an error mapping. - Wrote response bodies for the twelve routes that had none. Only the session
created by
POST /v1/sessionshad one anywhere in the corpus. - Derived the wire error vocabulary from the error taxonomy that already exists
rather than inventing a second one. Section 16's single worked example,
tool_validation_error, isToolValidationErrorsnake-cased; applying that convention to the thirty-one classes produces the code list, and four API-specific codes cover conditions that have no domain class. Four classes deliberately never cross the boundary, and an unmapped class isinternal_errorand 500. - Specified authentication at what it produces — a
Principal— and not only at what it refuses. No handler reads a tenant from a request. Scopes are exact-match strings over a closed dotted vocabulary, with no hierarchy in whichrun.writeimpliesrun.read. A resource in another tenant is 404, never 403, and scope is checked before tenancy so that no principal can probe for existence by watching 403 become 404. - Specified the consumer side of the event stream. Transient frames carry no
id, because the EventSource specification advances a client's last-event-ID only on a frame that has one, and a synthetic id on a token delta silently corrupts every later reconnect. Replay subscribes before it reads, buffers arrivals, reads the persisted prefix, and drains the buffer by sequence, which is what makes it gapless and duplicate-free. - Closed the cross-process cancel path. The endpoint writes
runs.cancel_requested_atand returns 202 for aRUNNINGrun, and transitionsQUEUEDand bothWAITING_*states directly toCANCELLEDwith 200 — which is safe only because those states hold no lease. - Separated the two mechanisms called "idempotency key": the HTTP header on
submission, and
tool_invocations.idempotency_key. Different scopes, different tables, different milestones, one unfortunate name. - Declared
SessionStatus, which Section 5 references and no document declared. Uppercase, to matchRunStatusand the guarded updates in the DDL. Section 16's lowercase sample is read as illustrative and Section 16 is not edited; the disagreement is recorded as an open question. - Added one route,
GET /v1/sessions/{session_id}, so that a client reconnecting with only a session identifier can learn the session's status and find its active run. - Recorded nine contradictions the expansion found, in the document's own
Contradictions resolvedtable. One of them narrows the readiness review: the cross-process cancel path was not unspecified, only its API half was. - Added
docs/adr/0028-http-api-and-streaming.md— seventeen decisions, eight consequences, fourteen alternatives considered. - Declared ten hard gates, all at Milestone 5, taking that milestone from one
gate to eleven and adding an eleventh area,
api, to the registry. Registry entries go from ninety-four to one hundred and four. Updated the milestone map's area grammar, gate table, and census, the harness's gate table, and the three counts in the engineering plan. ADR-0027 is not updated, because its arithmetic is a record of what was true when it was decided. - Updated
docs/plan/readiness.md: Milestone 5 moves from plan-only to ready. The original finding is kept rather than deleted, because it is the evidence for why the document was written and the record of what writing it turned up.
2026-07-25 — The readiness review¶
- Added
docs/plan/readiness.md, which traces every#### Implementbullet, acceptance criterion, and milestone subsection in the engineering plan to the document that designs it, and states a verdict per milestone. It owns no requirement and resolves no conflict. - The verdict: Milestones 0 through 4 are implementable from the corpus alone; Milestone 5 is implementable from the engineering plan but from no detailed-design specification; Milestones 6 through 10 are not implementable without design work that has not been done.
- The gate census in
milestone-map.mdwas used as an independent check on every verdict. It was derived mechanically before the review began and it agrees with the review everywhere: Milestone 5 registers one gate and Milestone 6 registers none, which are the two milestones the tracing found least covered. - Named the two documents that do not exist and should: an API specification
expanding Section 16, and a sandbox specification expanding Section 28.
tool-system.md:977already depends on the second by name. - Named the third-largest gap: skills have no design at all.
SKILL.mdappears only in the engineering plan and ADR-0013, and the acceptance criterion "A selected skill is version-pinned in the run" has nothing behind it, while Section 30 is referenced from eleven places as though the mechanism were settled. - Recorded the four plan sections no specification expands — 28 sandbox, 29 multi-device, 30 self-improving skills, and 31 trajectory export, the last of which is fully designed on the consuming side by the evaluation harness and not at all on the producing side.
- Recorded that the twenty-five initial evaluation cases carry milestones 1, 2, 4, 5, and 6 and no others, so Milestones 7, 8, and 9 have acceptance criteria with no case backing, and that two criteria — the container-escape red-team test and path-traversal rejection — stand on algorithms no document specifies.
- Reported four milestone conflicts without resolving any:
sandbox.run_commandat Milestone 5 or 6, usage token classes at Milestone 2 or 3, whether the HTTPIdempotency-Keyand the Milestone 1 idempotency port are one mechanism, and the container-escape test the case table does not contain. - Wired the document into the nav and recorded six judgment calls in
docs/status/questions-for-review.md, three of them carrying questions. - Added a
readinessblock todocs/status/project-state.yamlrecording which milestones the corpus covers and which three documents must exist before coding reaches Milestones 5, 6, and 8. The block records coverage; it does not authorize work, andauthorized_milestonesis unchanged at 0 and 1. Setlast_reviewed_commit, which had been null since the file was created. - Rewrote the required reading order in
AGENTS.mdto name the detailed-design documents, the milestone map, and the readiness review, and added a routing table from subject to document — fourteen rows, plus the statement that the HTTP API, sandbox isolation, and skills have no such document. Added a scope rule: do not begin an undesigned milestone by inventing the missing design. - Pointed
CLAUDE.mdand the project-state overview page at the readiness review, so an agent checks what is known to be missing before concluding that something is missing.
2026-07-25 — Gate milestones and the milestone map¶
- Added
docs/plan/milestone-map.md, which decides when each stated requirement must hold and states no requirement of its own. Recorded as ADR-0027. Registry rule 1's docs check is a Milestone 0 deliverable and it could not be written against the corpus: the gate section was spelled three ways, gates were declared two ways,milestonewas absent from nine of the ten declaring specs, and three gates were declared twice. - Assigned a milestone to every declared gate. Eighty-nine across ten specs, one more the engineering plan declares, and seven the map declares over the corpus: ninety-seven declarations, ninety-four registry entries once the three aliases are subtracted. The rule that produced every assignment is stated so the next gate added has an answer before the argument starts — a gate lands at the milestone that builds the last thing it observes — with the deliberate exception that a structural check vacuously true of an empty repository is registered at Milestone 0.
- Unified the declaration form across ten specs: one heading (
## Hard gates), one form (numbered items with a bolded lead), one suffix (**M<n>.**). Five headings renamed, four bullet lists converted, and seventy-five milestone tokens added. No sentence stating a requirement changed. - Separated gates from tracked metrics. Nine bullets carrying eleven metrics
moved into new
## Tracked metricssiblings in five specs, leaving fourteen gates in the two lists that had interleaved them. The split was made on the specs' own words, not on a judgment about which items sounded gate-like. - Gave each of the three double-declared gates one owner and an explicit
alias. The import-boundary walk is owned by the engineering plan's Milestone
0 — the one registry entry whose owner is not a detailed-design spec —
and transaction hygiene and checkpoint dispensability are owned by
event-log-and-persistence.md, withtool-system.mdandruntime-loop.mdrestating them. - Resolved three placements the specs contradicted each other on: the
idempotency port is Milestone 1 and its unique index is Milestone 2, with
the in-memory adapter declaring the gap rather than simulating a race;
Milestone 1 cancellation is a lazily evaluated deadline plus a
SIGINThandler, withCancelReasonsplit by dependency across Milestones 1, 2, and 5; and the context engine's determinism gate moves to Milestone 1, where ADR-0024 had already scheduled its subject. - Added
optionalto the registry for exactly one gate — the model gateway's live vendor smoke test — bounded in writing to external credentials. - Corrected the evaluation harness's own gate table. Its counts covered six specs when six existed; it gains rows for the engineering plan and the map, and its memory-formation count of seven becomes five. More than half of the declared gates are not case gates.
- Two findings reported rather than fixed: Milestones 6 and 8 add no gates, and thirty-eight of ninety-four gates are green before Milestone 2, eleven of them against a repository with no agent in it. The second is the argument for building the in-memory tier as real adapters.
- Wired the cross-references: Section 20's stale count corrected, and new paragraphs under Section 21, Milestone 0, and Milestone 1. No deliverable or acceptance criterion in the plan changed.
2026-07-25 — The builtin tools¶
- Added
docs/plan/builtin-tools.md, the design for the two tools Milestone 1 cannot be written without and the classification for the six that arrive later. Recorded as ADR-0026.math.calculatehad five bullets, two of which said what not to do;system.current_timehad four, one of which said "Deterministic" about a tool that reads a clock. - Reconciled the roster. Section 8.1 ends with seven namespaced names and
Section 8.2 specifies six tools —
demo.external_writeis in 8.2 and not 8.1,artifact.exportis in 8.1 and specified nowhere. Read as two rosters they contradict; read as a naming convention illustrated by example and a list of what the early milestones build, they do not.tool-system.md's domain partition already reservesdemo, which settles which reading was intended. The roster is the union: eight tools. - Placed
artifact.exportat Milestone 6, the one tool the plan assigns to no milestone, with the two rejected alternatives and their reasons. - Gave every one of the eight a value for every
ToolSpecfield — side-effect class, risk, idempotency, trust label, scopes, timeout, output ceiling, parallelism, kind, source, and execution target. Hard gate 1 refuses to start withoutoutput_truston every registered spec, and not one builtin in the corpus declared one. - Specified
math.calculatecompletely: an eight-production grammar, a hand-written tokenizer and precedence-climbing parser rather than an allowlist overast.parse,decimal.Decimalat fifty significant digits withROUND_HALF_EVEN, the operator set with^and**unified and//and%flooring rather than truncating, ten functions and two constants, four bounds, eight reason codes with their checked-in messages, and both JSON schemas. - Made
9**9**9a failure rather than an outage, and made it one without a check in the evaluator:EmaxandEminat ten thousand withOverflowandUnderflowtrapped means the decimal context refuses from the exponents before a digit is computed. - Specified
system.current_timecompletely: IANA names only, defaulting toUTCrather than the host's zone, resolved throughzoneinfowithtzdataas a declared dependency, five output fields, and an ISO string that always carries a numeric offset so it is byte-stable under a fixed clock. - Relocated Section 8.2's determinism claim to where it can be true. The tool
is a pure function of
Clock.now(), the timezone argument, and the timezone database; determinism is a property of the port, not of the tool. An implementation callingdatetime.now(UTC)satisfies all four of Section 8.2's bullets and is untestable. - Closed an open declaration in
runtime-loop.md:Clock.now()returns an awaredatetimein UTC, asserted in the port's contract suite. Every consumer so far compared two values from the same clock, so the ambiguity was harmless until a consumer converted between zones. - Closed a hole in hard gate 2. It forbids trust above
EXTERNAL_UNTRUSTEDfor tools whosesourceismcp,device, orsandbox;sandbox.run_commandis a builtin withtarget_kind = sandbox, so the gate as written did not reach the one builtin that returns bytes produced by code we did not write. Restated over both fields. - Established the failure-message rule for builtins: the reason code carries the diagnosis, the message carries the remedy and the supported set, and neither carries the input. Hard gate 4's message table is shared with MCP tools whose failure text is attacker-controlled, and a table with one interpolating entry has no invariant left.
- Specified registration as seven ordered refusals in the composition root's freeze phase, pure and testable with nothing else constructed.
- Added nine hard gates, four of them adversarial: a property test that no
generated expression escapes the eight reason codes, timing assertions on
9**9**9and its neighbours, a differential test pinning//and%against Python's integer operators, and0.1 + 0.2returning exactly0.3, which is the entire argument forDecimalin one line. - Added four additive cross-reference paragraphs to the engineering plan
(Sections 8 and 8.2, Milestones 1 and 4) and twelve entries to
questions-for-review.md. No requirement was rewritten.
2026-07-25 — The development toolchain¶
- Added
docs/plan/development-toolchain.md, the design for the nine Milestone 0 deliverables that were one line each. Recorded as ADR-0025. "Makefile" was one word; "Docker Compose with PostgreSQL" named no version, port, volume, or database; "CI pipeline" named no workflow file; "Structured logging bootstrap" occurred once in the entire documentation set. - Resolved the collision between "CI executes
make check" and the evaluation harness's four CI jobs, one of which needs PostgreSQL and one of which needs a provider credential.make checkislint typecheck test-fast,test-fastistest-staticfollowed bytest-contract, and those two are CI jobs 1 and 2 exactly — a partition, not a duplication. CI then runs the two jobs that need resources. Both corpus statements are true afterward and neither was weakened. - Resolved the second collision underneath it: Section 21 requires both
make testandmake check, and ifcheckcontainedtestit would inherit the database requirement and fail on a fresh checkout, contradicting Section 24's definition-of-done item thatmake checksucceeds.checkdepends ontest-fast;teststays the broader local target it reads as. - Fixed the governing rule: CI runs no command the Makefile does not define. The workflow file is a schedule and an environment, not a second definition of what the project checks. Six targets were added to Section 21's eight for that reason alone, and each exists because a CI job invokes it.
- Specified every Makefile target body: what all fourteen run, why
db-uppolls the compose healthcheck rather than sleeping, and whydb-upandmigratestay two commands — Section 25 documents them separately and ADR-0024 forbids migrating from the composition root. - Specified the compose file:
postgres:16-alpinepinned rather than floated because the persistence layer depends onFOR UPDATE SKIP LOCKEDsemantics, one service, named volume,pg_isreadyhealthcheck, and credentials that live in.env.exampleand pass the secret scanner by an allowlist entry carrying a prose reason. - Specified the CI workflow: one file, four jobs matching the harness, one
Python version rather than a matrix,
uvcache keyed on the committed lockfile, and a live job that runs on schedule and manual dispatch only — never on a pull request, because a fork cannot hold the credential.mkdocs build --strictruns inside job 1 rather than in a job of its own. - Specified structured logging: structlog, configured in phase 1 of the
composition root, two renderers keyed on deployment mode, Section 19's eight
fields bound as context variables at four named points, and
trace_idread from the active span rather than threaded through call sites. Redaction is a processor in the chain rather than a convention, with content keys truncated to 200 characters instead of dropped. - Mapped each of the six test directories to a marker, which is the piece
that was missing: the harness had already named
resilienceas the sixth category, but nothing said how a category is selected at the command line. Three of the six carryintegrationbecause all three need a database. - Reconciled Milestone 1's "Deterministic tests" with the harness's "cases 1 through 11" as one deliverable. Reading them as two is how a milestone acquires a second, informal test framework beside the specified one.
- Resolved "Initial ADRs", which had three defensible readings — the six filenames in the Section 4 tree, the eleven a note defers to their milestones, or the twenty-five that now exist. Milestone 0 carries the accepted set forward whole and authors nothing new; numbering continues from the highest number carried over.
- Placed the Milestone 0 egress block, previously attributed to that milestone only by a non-authoritative status document. It is an autouse pytest fixture of about thirty lines — no firewall, no container network policy — and it is what turns the harness's "runs without an API key" claim into a test.
- Placed
docs/security.mdat Milestone 0. No milestone claimed it, but Section 24 requires security implications to be documented for every milestone, and Milestone 0 already ships two security controls. This adds no deliverable; it identifies where an existing item lands. - Left
docs-manifest.yamlat four sources and recorded why. Widening the single-file HTML publication to the full corpus requires per-document anchor prefixing inscripts/build_docs.pyfirst: thirty-seven documents share heading names like "Decisions", and the anchor generator resolves duplicates to the first occurrence, so a naive widening produces cross-references that silently point at the wrong document. - Added six additive cross-reference paragraphs to the canonical plan (Sections
19, 20.4, 22, 24, 25, and Milestone 0), wired the new spec and ADR into the
MkDocs nav and the ADR index, and recorded twelve judgment calls in
docs/status/questions-for-review.md.
2026-07-25 — Bootstrap, configuration, and the composition root¶
- Added
docs/plan/bootstrap-and-composition.md, the design for the process that constructs everything the other nine specs describe. Recorded as ADR-0024. Nine specifications described nine mechanisms; none described the interval before any of them runs. - Gave configuration a shape. The corpus declares 106 knobs and the plan
names three environment variables. A value is an environment variable if and
only if it differs between two deployments of the same revision and cannot be
committed — which leaves eight fields in
Settingsand puts the rest in six committed YAML files, each beside the package that reads it, with an optional operator overlay directory merged over them. - Fixed the reading of "New configuration appears in
.env.example" that would have made all 106 knobs environment variables. That reading contradictspolicy_version, which is a hash of the files the rules came from: an environment variable that changed an effective rule would leave the hash untouched, turning the audit trail from stale into false. The environment never overrides a file; it is interpolated into one at named points, generalizing themodel: ${OPENAI_MODEL}form Section 10.5 already uses. - Introduced
DATABASE_URL, which no document named, for the PostgreSQL instance the plan makes the source of truth. - Assembled startup order, stated seventeen times across nine documents and
composed nowhere. Five phases, ordered by what each may touch: refuse
(settings only), determinism (
ClockandIdFactory, before anything reads ambient time), resources (a session factory, never a session), freeze (every versioned asset loaded, hashed, and made immutable, hardline first), and wire. All seventeen constraints land in one of the five, and the three that read like startup constraints and are not — migrations, provider pinning, contract coverage — are named as such. - Specified
buildas one async context manager whoseCompositionexposes application services and nothing else. No adapter, repository, or session factory is reachable from an entry point, which is how ADR-0023's reservation ofRunRepository.transitiontoruntime/executor.pysurvives a second one. - Gave Milestone 1's three bodiless bullets bodies. "In-memory
repositories" is five adapters in
adapters/persistence/memory.py, run against the same contract suites as their PostgreSQL counterparts rather than living undertests/as doubles; there is no in-memoryRunQueue, because that port's entire content isFOR UPDATE SKIP LOCKEDand lease fencing. "Inline run dispatcher" is a one-methodRunDispatcherwhose postcondition both adapters satisfy. "Minimal context builder" is context-engine build-sequence step 1, with its stability test stated as two assertions rather than one. - Resolved two milestone conflicts by separating two words each. An event repository is Milestone 1 and append-only event storage is Milestone 2 — one port, two implementations. The transaction-hygiene check is a Milestone 0 deliverable and the gate is a Milestone 2 criterion, because Milestone 0 has no database code to walk.
- Completed the Section 17 CLI contract: arguments, options, stdout versus
stderr, six exit codes, and the milestone each command first works at.
get,events, andcancelare reserved words afteragent run, with--as the escape, which is what makesagent run "get the weather"decidable. - Specified the secret scanner dependency rule 12 and Milestone 0 both name
and neither describes: five rule families, a report that never prints what it
matched, an allowlist whose entries require prose, and
.env.examplescanned rather than exempted. - Added four static checks to the Milestone 0 import-boundary walk, each true
of an empty repository and still true as it fills:
bootstrapis imported only by the three entry points, no module outsidebootstrap.pyinstantiates an adapter, no module outsideadapters/determinism.pyreads ambient time or generates an identifier, and noAsyncSessionexists at module scope. - Annotated the Section 4 tree rather than redrawing it: eleven files added,
one name retired.
runtime/engine.pybecomesloop.py,executor.py, andsupervisor.py;ports/gainscontext.py,memory.py, anddeterminism.py;adapters/models/gains the Anthropic, OpenAI-chat, and local-endpoint adapters ADR-0002 and ADR-0012 require and the tree never listed. - Recorded that
ModelProviderwas declared twice with incompatible shapes, in Section 7 and inmodel-gateway.md, and made the gateway's version canonical. Section 7's is annotated as superseded and left in place, the same treatmentRunRepository.claim_nextreceived. - Fixed a rendering defect in Section 5: the Pydantic note between rules 1 and 2 split the ordered list, so the fourteen dependency rules rendered as 1 and then 1 through 13 — meaning every reference to "rule 14" in the corpus pointed at a line the reader saw numbered 13. The note is now indented as continuation text under rule 1, which is the rule it is about. No rule text changed. A sweep of all forty-one built pages found no other interrupted list.
- Sixteen further judgment calls are recorded with their reasoning and reversal cost in questions-for-review.md.
No product implementation was performed.
2026-07-25 — Cross-document defect sweep before the readiness review¶
- Read the nine detailed-design specs against each other rather than against the plan, looking only for places where two documents state the same fact differently or where one names something no document declares. Ten defects were found and all ten are fixed. Each is recorded with its reasoning in questions-for-review.md.
- Declared
RunStatus— seven members — which five documents referenced and none defined. A run blocked on a child waits inWAITING_FOR_USERwith a suspension record naming the child, rather than in an eighth state that only the suspension record could distinguish. - Added the four
runscolumns the domain model always implied and the schema never carried:tenant_id,agent_id,agent_version, and a nullabledeadline_atwith a partial index restricted to the three live statuses. - Resolved
tool_invocations.origin_trust, declared nullable bytool-system.mdandNOT NULLbypolicy-and-approvals.md, in favour ofNOT NULL. A call the runtime issues itself carriesPLATFORM. A nullable column would let an authorization record say "policy did not compute this", which is the one thing it must never be able to say. - Renamed the classification column to
idempotency_class, because the same table already carriesidempotency_keyand crash recovery reads both — one to decide whether replay is safe, the other to decide whether it is the same call. - Corrected the step identity claim:
step_numberis canonical onmodel_calls, not ontool_invocations, because a step that proposes no tool calls writes no invocation row and would therefore be skipped by any numbering taken from that table. - Completed the event catalog at fifty-one types. It had gone stale at the twenty-four of Section 6.8 while five later specs added to it, including seven MCP and bridge events and six memory events. Stating the total makes the next addition visible.
- Fixed three assertions that name things which do not exist: the harness case
approval_granted_resumes_runnow assertsrun.queuedandapproval.resolved, and the gate id standardises on the registry spellinggate.policy.prompt_is_not_authorization. - Recorded, without deleting them, that
RunRepository.claim_nextandheartbeatare superseded byRunQueue.claimandRunQueue.heartbeat. The signatures stay in Section 7 as the record of what was replaced. - Corrected two cross-references in
model-gateway.mdthat cited Section 10.3 for the normalized request; it is Section 10.1. - Brought every fenced code block in the corpus within the rendered code column.
Verified by measuring
scrollWidthagainstclientWidthfor everypreon all thirty-nine built pages rather than by counting characters: zero blocks overflow. The first-line allowance was re-measured too — the copy control in the current Material release sits in its own navigation bar, so a first line has the same budget as any other line, not a shorter one.
No product implementation was performed.
2026-07-25 — The runtime loop, the step, and the single terminal writer¶
- Added
docs/plan/runtime-loop.md, the design for Sections 12 and 13 and the loop-facing halves of 6.4, 6.5, 6.9, 14.1, 14.2, 16, 19, 26, and 27. Recorded as ADR-0023. This is the last of the eight specs and the seam the other seven are called from. - Established the finding that drives the document: Section 12.1's forty-two
lines name eleven callables and not one of them is a declared port. Section
7 declares eight port Protocols; the only one the loop touches is
context_builder.build. Each of the other eleven names is a seam where two specifications' requirements meet, and at most of them the two disagree. - Found three places where the loop as written cannot do what another document
requires of it. It cannot resolve its own agent, because
engineering-plan.md:1408, as the plan then stood, readsagents.get_version(run.agent_id, run.agent_version)and Section 6.3 puts both fields onSession. It cannot suspend, becauseengineering-plan.md:1437, again as it then stood, returns bare while Section 27.2 requires the lease released, a checkpoint written, and an event emitted — so a run paused for approval holds its lease until expiry, at which point the queue hands it to a second worker. And it cannot compact, because Section 11.4 requires compaction, Milestone 7 gates it, and no pressure measurement or compactor call exists anywhere in the corpus. - Split the runtime into a
run_loopthat computes a typedRunOutcomeand afinalizethat performs every terminal action once. The suspension defect is not fixed by adding afinallyto the flat loop, which leaves the nextreturnfree to reopen it; it is fixed by moving the ending somewhere areturncannot skip. A structural gate asserts thatRunRepository.transitionandRunQueue.releaseare reachable from exactly one module. - Defined
Stepas a runtime value object with a persisted identity — one additive column,model_calls.step_number— rather than as a counter or a table. Steps that produce no tool calls are currently invisible; they are the ones a terminal turn is made of. - Added nine fields to
Run, six of which the event-log spec already introduced as columns and never reflected in the domain model, and three of which —agent_id,agent_version,deadline_at— are denormalized so the run is self-describing and the deadline is indexable by a sweep. - Gave compaction a call site:
build_with_pressure, which measures, invokes the compactor if the body will not fit, adopts the checkpoint it returns, and measures again, capped at two per step withContextOverflowpermanent on the third.build()stays pure, which is what the byte-identical rebuild on the step-retry path depends on. - Resolved budget into three scopes — run, step, attempt — and ruled that Section 6.5's "after" means record: usage is recorded and the limit is evaluated in one transaction, because splitting them opens a window in which the run is over budget and nothing knows.
- Made the heartbeat a supervisor task rather than a statement in the loop,
running at a third of the lease interval and watching three things that are
all "has the outside world changed its mind": the lease, the deadline, and a
cancellation request.
heartbeatreturnsbool;Falsemeans fenced. Every non-append write the loop makes is guarded byWHERE lease_epoch = :lease_epoch. - Specified cancellation as one token per run with six observation points
shared by the loop, the tool executor, and the sandbox, and one rule about
effects: a cancellation observed after a call's
effect_sent_atwatermark is set completes the disposition rather than abandoning a half-sent side effect. - Resolved the checkpoint's two descriptions as two types rather than a
contradiction: Section 6.9's inline
conversationis what the repository returns, and event references and deltas are what it persists. Added the six triggers, thefullrule the call site can evaluate,checkpoints.fullandcheckpoints.base_version, andseed_checkpointas a function with two call sites so a run whose checkpoints are all deleted still reaches the same terminal state. - Specified the resume ladder: resumption is a cold process start and a warm
pipeline entry. The worker claims a
QUEUEDrun from scratch; the pipeline re-enters at step 6 for each pending call, so a call whose watermark was set is not re-executed.run.resumedis emitted whenever the execution did not start the run, which covers all four paths without enumerating them. - Ruled that an empty terminal model turn is a failed step, retried and
failing the run with
EmptyModelTurnon exhaustion, rather than a completed run whose final message is empty. - Added
FailureReason, fourteen values, so aFAILEDrun distinguishes a provider outage from an exhausted budget from a policy denial, and named the owner of every retry: the adapter's transport retries, the gateway's attempt loop, and the runtime's step retry, split onstream_had_output. - Listed and resolved twenty cross-document contradictions by number, naming
the losing side in each. Three needed argument:
RunUsagehas five token classes and the event log's "unchanged in shape" is about the rollup relationship; Section 6.8'stool.call.*names are canonical and the evaluation-harness case assertingtool.proposed/tool.authorized/tool.succeededis corrected in that document; and Section 27.5's "reject or queue" resolves to reject with HTTP 409, because ADR-0004's partial unique index makes queueing impossible at the database level. - Added fourteen hard gates, twenty-five decisions, the events consolidation
that introduces no new event type and assigns owners to
run.claimedand the tworun.waiting_*events, the three-role deployment shape with the sweeps under advisory locks, and the transaction boundaries of every write the loop makes. - Wired the nav, the ADR index, and additive cross-references from Sections 12,
13, and 27 and from Milestones 1, 2, 4, 5, and 7. No requirement was
rewritten: Section 13's retry table keeps every row and its retryability, the
Section 8.3 pipeline steps are not reordered, Section 7's
build()signature is unchanged, and 27.1's definitions — including that a turn has no domain object — stand. - Recorded further decisions in
docs/status/questions-for-review.mdand six open questions in the spec, of which the two with the highest reversal cost are whether cancellation ships in three milestone slices and whether a child-run wait deserves its own run status.
No product implementation was performed.
2026-07-25 — The evaluation harness, the gate mechanism, and ADR-0001¶
- Added
docs/plan/evaluation-harness.md, the design for Section 20 and for the gate sections the seven sibling specs close with, plus the parts of Sections 3, 4, 10.3, 19, 21, 22, and 31 they depend on. Recorded as ADR-0022, with the boundary-enforcement half recorded as ADR-0001. - Added
docs/adr/0001-modular-monolith.md, closing the numbering gap the ADR index had been apologizing for. Section 4's repository layout names the file and twenty ADRs referred to a decision documented nowhere. It records the modular monolith as accepted, defines replaceability as a port with a contract suite rather than as a service boundary, and resolves Section 5's "where practical" rule by rule: eight of the fourteen dependency rules by import graph, four by other static checks, one by the secret scanner, one by adapter registration, and two residues named as not mechanically checkable with their compensating controls. - Established that Section 20's harness cannot run the gates six specs assign to it. The specs declare roughly forty-nine hard gates and the policy spec names Section 20 as their enforcer; Section 20's sixteen assertion types can express perhaps eight. Sorting the declared gates by kind gives the finding that drives the document: roughly a third of them are not case gates, so a case-only harness reports a green build with a third of the plan's stated invariants unchecked.
- Defined a hard gate as a named, executable, milestone-attached condition that fails the build, and gave gates a checked-in registry — one YAML file per spec area, carrying the identifier, milestone, kind, a verified link back to the declaring document, the prose statement, and the check that implements it. A gate declared in a spec but absent from the registry fails the docs build; a gate at or past its milestone may not skip.
- Added the three gate kinds the case format cannot express: property gates
with a generator, a predicate, a minimum trial count, and a recorded failing
seed; corpus gates with a
minimum_membersfloor so they cannot pass vacuously; and structural gates that never run the agent. The last two need no runtime and are Milestone 0 work. - Defined "deterministic", which the plan asserts and never defines, by naming its seven sources and their treatments: the clock, identifiers, and model output are pinned behind ports; batch concurrency and database row order are ordered by explicit keys; retry timing is bounded; hash iteration order is accepted and never asserted on. Stated the limits as plainly — the runtime is not deterministic, payloads are not byte-identical, and the deterministic scheduler proves the parallel path produces the right result, not that it is race-free.
- Resolved
model_fixture, which Section 20.1 names with a bare string and nothing defines: it resolves toevals/fixtures/models/NAME.yaml, validated at collection against the currentFakeModelScriptshape rather than at run time. Recording is an explicit command and never a side effect of running the suite. - Added
interventionsto the case schema — approve, deny, cancel, kill the worker, answer, disconnect — without which the eight Section 20.3 cases that involve approval, restart, cancellation, or disconnect are unwritable. Every field Section 20.1 already had is unchanged. - Made "no unauthorized side effects" decidable. It was the last assertion
type in Section 20.2 and it asked the harness to prove a negative about a
system it does not control. It is now asserted against ADR-0021's
tool_invocations.effect_sent_at: an emptyexpected.effectslist means no invocation in the run set a watermark, and it is the default. - Ruled that there is no test mode. No environment variable, no flag, no
if settings.testingbranch in the policy engine, the approval service, or the tool executor. Evaluation identity is data — atenant_evaltenant under ordinary row-level security, named principals with real scopes, and policy profile files loaded by the same loader and subject to the same totality gate. Anapproveintervention calls the real approval service as a second principal rather than setting a status, and the structural form of the rule is that no module underagent_coreoutsideagent_core.evalsmay importagent_core.evals. - Bound contract suites to ports rather than to implementations, so the
model gateway's second gate — the same contract passing against fake,
recorded, OpenAI, Anthropic, and
chat_completions— is one configuration line per adapter instead of five files that drift. A port with no contract module, or an implementation not registered against its port's contract, fails the build. - Named
resilienceas the sixth test category. Section 20.4 lists five; Section 4's layout has six directories and two specs already place tests in the sixth. Added the routing rule that keeps "integration" from becoming the drawer everything slow ends up in, and stated that eval cases are not a seventh category. - Gave every one of Section 20.3's twenty-five cases an earliest milestone and a statement of what only that case proves. Ten are writable in Milestone 1, which is what makes "build evaluations before advanced features" a schedule rather than an aspiration. Split case 18 into 18a and 18b around the effect watermark, since only the second — asserting that the model is told the outcome is unknown rather than that the call failed — has a safety consequence.
- Specified the capability track's governance: scenarios have no assertions and default to five repeats; a judge is a model, prompt, and rubric versioned as one unit and pinned to a provider version, replaced only alongside a bridge run, never shown the rubric's weights, and never reusing an identifier after deprecation; cross-version score comparison is refused by the tooling. A regression is a distribution change — floor drops and policy failures block a release, mean drops inside a measured noise band do not — and a capability improvement that increases policy failures is a regression.
- Ruled that a scenario hitting a cost ceiling is excluded from the score distribution rather than scored zero, since it conflates "we stopped paying" with "the agent failed" in the direction that corrupts the distribution most.
- Designed the trajectory-to-case conversion that Section 31.3 asserts and
nothing specified, and stated its lossy boundary field by field. Recorded tool
results are replayed rather than re-executed; timestamps, identifiers, usage,
and cost are discarded; redaction happens at export so there is one place the
rule lives; and a converted case is marked
source: trajectoryand stays out of the blocking suite until a person writes its assertions. - Added a flake policy with an expiry: retry once with the retry rate reported even while green, quarantine on a second failure within thirty days, and automatic release after fourteen days whether or not the test was fixed. Gates may never be quarantined.
- Added ten hard gates of the harness's own, ten build-order steps with the first two in Milestone 0, six metrics, four event families, two tables for the capability track and none for the deterministic suite, an eight-row import-boundary table, and ten failure modes with their mitigations. Gate 7 asserts the definition of done's eighteenth item by running CI with egress blocked rather than by not configuring an API key.
- Wired the nav, the ADR index, and additive cross-references from Sections 2.1, 5, and 20 and from Milestones 0 and 1. No requirement was rewritten: the twenty-five cases stay twenty-five, the sixteen assertion types stay and gain four, the capability track stays non-blocking, and Section 5's fourteen dependency rules are unchanged.
- Recorded seventeen further decisions in
docs/status/questions-for-review.md, including the ADR renumbering that moves the runtime-loop record to ADR-0023, and five open questions in the spec.
No product implementation was performed.
2026-07-25 — The tool system, the execution pipeline, and MCP¶
- Added
docs/plan/tool-system.md, the design for Sections 7, 8, 12.4, 12.5, 15'stool_invocations, and Milestones 1, 4, 6, and 8: the execution pipeline, idempotency and recovery, output limits, control tools, the orchestration bridge, and the MCP adapter. Recorded as ADR-0021, constrained by ADR-0015, ADR-0008, ADR-0005, ADR-0006, ADR-0002, ADR-0013, and ADR-0020. - Defined the two types the Section 7
Toolport names and nothing in the plan, the six sibling specs, or the twenty ADRs declared:ToolResultwithToolFailureandToolFailureKind, andToolExecutionContextwith its sixteen fields. A tool returnsokand content, never a status, because status is the pipeline's judgement and a tool able to claimdeniedcould launder a denial. - Ruled that
ToolExecutionContextcarries no database session, no repository, no policy engine, and no registry, so Section 12.2's "no transaction across tool I/O" is enforced by construction and a tool cannot call a tool. - Completed
ToolSpecwithkind,target_kind,output_trust,source,server_id, anddeprecated, and addedToolKind,ToolSource, andToolOutcomeStatus.output_trustis forced toEXTERNAL_UNTRUSTEDat registration for every MCP, device, and sandbox source, which also resolves the plan's two conflicting defaults forToolResultItem.trust. - Gave the registry a name grammar and a reserved-domain partition, and
named MCP tools
mcp.{server_id}.{normalized_remote_name}by a deterministic five-step normalization. This is the join Milestone 8 needed and lacked: without it every discovered MCP tool is an unknown tool and Section 9.2'sUnknown tool -> Denyrow makes the milestone non-functional. - Made the pipeline fourteen steps, one function, one call site, preserving Section 8.3's ten in order and inserting the four persistence points Section 12.2 requires. Nothing before step 8 has touched the world; nothing after step 10 can undo step 10. The bridge, the device channel, and the MCP adapter re-enter this function rather than implementing variants, which is what makes ADR-0015's "the bridge is the enforcement point" true rather than aspirational.
- Added
effect_sent_at, a nullable timestamp written immediately before a tool's first outbound operation, and turned the event-log spec's undefined word ambiguous into a fact a recovering worker can read. Section 8.4's rule against auto-retrying non-idempotent calls is preserved exactly for calls that may have escaped, while a worker that died during argument marshalling is retried instead of escalated. The honest limits — that a set watermark proves the effect may have happened, and that the false-positive case costs one human review — are stated in the document rather than left to be discovered. - Derived the idempotency key from Section 8.4's five inputs plus a version tag
and
tool_version, and excludedattempt_numberso a bounded retry reuses the key and the external service can deduplicate it. - Defined where
argument_trustandorigin_trustcome from, which the policy spec consumed and nothing produced.origin_trustis the minimum label over the request's context items;argument_trustdefaults toEXTERNAL_UNTRUSTEDand is only ever raised, on a verbatim sixteen-character match againstUSER-labelled context — a direction chosen so the failure mode is an unnecessary approval rather than an injection passing as trusted. - Ruled that large output is excerpted head-and-tail and artifactized, never summarized, since a summarizer over untrusted tool output is the laundering ADR-0020 forbids, and that an excerpt never splits a trust envelope.
- Gave all four non-success outcomes one six-field shape with a stable
reason_codeand a fixed message, and ruled that external text is data and never narration: a remote system's error string is enveloped untrusted content, never the outcomemessage. - Unified the policy spec's repeated-denial breaker and Section 12.5's loop
detector into one counter at three thresholds, adding the rule that an
invocation which resolved
UNCERTAINis never proposed again in the run. - Defined a step as one model call plus the complete disposition of every
tool call it produced, so a batch shares a
step_numberand a crash-retry derives the same keys. Parallelism admits or rejects the whole batch; a mixed batch runs sequentially rather than being split, because splitting requires the independence inference Section 12.4 warns against. - Made control tools a declared kind that runs the full pipeline and suspends
through a nullable marker rather than a new status, so no consumer of
tool_invocations.statuslearns about suspension and the lease reaper excludes them by predicate. - Specified the MCP adapter to one principle: a server does not classify
itself.
side_effect,risk,idempotency, andrequired_scopescome from operator configuration, an unclassified server is maximally restricted, tenant-configured servers are HTTP-only through the egress allowlist, and stdio servers — child processes in the worker's trust zone — are operator-configured only. - Mapped MCP's other surfaces deliberately rather than naturally: resources become one synthetic per-server read tool instead of an automatic context source, prompts become read-only untrusted skills that never enter the cacheable prefix, and sampling and roots are declined at capability negotiation so a server never gets to spend a tenant's model budget.
- Extended ADR-0020's pinned-advertisement rule to MCP catalog changes, so
tools/list_changedis recorded and not applied and an external server cannot invalidate a tenant's prompt cache at will. - Gave the orchestration bridge a synthesized
call_idfrom the script hash and a bridge-counted ordinal so a replayed script deduplicates, with the determinism caveat stated plainly, plus a bounded approval hold and a per-turn cap on underlying calls. - Added fourteen columns to
tool_invocationsand two new tables,mcp_serversandmcp_tool_catalog, the second a history rather than a cache. Declined to add atool_registry_snapshotstable, since the context plan's pin already answers what a session advertised. - Added seven event families, two span children, five metrics, an import boundary table, ten hard gates, and a nine-step build order that places most of the work in Milestone 1 rather than Milestone 8.
- Cross-referenced the new document from Section 8, Milestone 1, and Milestone 8's MCP subsection, additively. No requirement in the plan was rewritten, weakened, or reordered.
No product implementation was performed.
2026-07-25 — Model gateway and the provider-neutral protocol¶
- Added
docs/plan/model-gateway.md, the translation design for Section 10 and Milestones 1 and 3: the normalized stream, the two first adapters, routing, usage and cost, retries, and reasoning. Recorded as ADR-0002, constrained by ADR-0006, ADR-0007, ADR-0010, and ADR-0012. - Defined the roughly twenty types the plan used at call sites and never
declared: the five
ConversationItemmembers,ContentPartwith its text, image and file variants,PendingToolCall, the six streaming event classes,ModelUsage,CostSource,StopReason,ModelAttempt, the three error classes,FakeModelScriptwithScriptedTurnandScriptedToolCall, theUsageRepositoryport, andResolvedModelwithModelCapabilities,ModelLimits, andModelPricing. - Stated the six invariants of the normalized stream — contiguous sequence,
exactly one terminal event, contiguous ordered deltas per item,
call_idandnameknown at tool-item start, advisory usage, and no raw provider error text or credentials on any event — and gave them a shared validator, so a violation is an adapter defect rather than a caller concern. - Made one shared assembler fold events into turns for every adapter, which is what makes the contract suite a controlled comparison: the same code produces the turn on every provider, so a difference in the turn is a difference in the events.
- Resolved the nine unfilled cells of the Section 10.2 mapping table without
changing any mapping it already states:
server_tool_usebecomes a protocol error,content_block_stopandmessage_stopare structural,UsageEventis advisory so its missing OpenAI source stops mattering,response.incompletemaps toMAX_TOKENSwhere the cap is the cause, and the five OpenAI lifecycle events drive item bookkeeping only. - Gave in-band
<think>the mapping table ADR-0012 assumed and Section 10 never wrote, plus a streaming scrubber with one-token lookahead and a per-profile configurable tag pair, since open models do not agree on the delimiter. - Gave
cache_creation_input_tokensthe home Section 10.2 said to find for it:cache_write_input_tokens, a fifth tracked token class on bothModelUsageandRunUsage. Madereasoning_tokensNonerather than0where a provider does not itemize it, and moved the itemization question into pricing asreasoning_priced_separately. - Added the
ModelRouterport, turningmodel_policyfrom a bare string into aResolvedModelcarrying provider, model, capabilities, limits, pricing, and a credential reference. This givesModelCapabilitiesa resolution path and gives the context engine's "8,192 or the model's default" output reserve its missing second half. - Resolved provider pinning against availability routing temporally rather than architecturally: selection happens once at run start, the pin is absolute and persisted for the life of the run, and Milestone 10 routes selection and never live runs.
- Split retry ownership on
stream_had_output: the adapter retries only before the first event reaches the caller, at most three times; after any output it fails and the caller decides.max_attempts = 3lives in application code, matching the worker's existing figure. - Added the model-call timeouts no document carried —
timeout_secondsand the load-bearingstream_idle_seconds— and made cancellation produceStopReason.CANCELLEDon a partial turn rather than an error. - Bounded the
PLATFORMtrust default onProviderReasoningItemwith four properties that leave the label no consumer able to act on it: the payload is never parsed, never rendered as prompt text, never reaches the policy engine, and never enters memory or a user-facing renderer. The name is still wrong and is raised for review rather than edited. - Made the gateway enforce tool call and tool result pairing before sending, which the policy spec's denial-as-tool-result and the context engine's compaction atomicity both depended on and neither owned.
- Added
model_callsandmodel_pricesto the schema, one row per attempt and an append-only price history, so a three-month-old invoice stays reconcilable and per-attempt cost is a query rather than log archaeology.runs.usageis unchanged in shape and becomes a rollup maintained in the same transaction. - Made failed attempts count against budget, checked before each attempt, so a crash-looping run stops for a stated reason instead of being mysteriously expensive.
- Added event payloads for
model.request.startedandmodel.response.completedthat the context engine already consumed and Section 6.8 never specified, plus three telemetry attributes for the cached, cache-write, and reasoning token classes and amodel.attemptspan. - Specified ten hard gates, six tracked metrics, and a fourteen-step build order in which the stream validator and the contract suite are written before the first real adapter.
- Wrote ADR-0002 with twenty decisions and seventeen rejected alternatives, and wired it and the spec into the navigation, the ADR index, and Section 10, Section 15, and Milestones 1 and 3 of the engineering plan.
- Recorded twelve judgment calls in
docs/status/questions-for-review.md, flagging thePLATFORMtrust default as the one worth reading first. - No product implementation was performed.
2026-07-25 — Policy engine and approval lifecycle spec¶
- Added
docs/plan/policy-and-approvals.md, the authorization design for Sections 8.3, 8.4, 9, 11.2, 13, and 22 and Milestone 4: classification, the deterministic matrix, the hardline layer, and the approval lifecycle. Recorded as ADR-0005 and ADR-0006, constrained by ADR-0017. - Defined the five types the plan named as field types but never declared —
SideEffectClass,RiskLevel,IdempotencyClass,ProposedAction, andApprovalStatus— deriving each value from an existing plan statement rather than inventing a taxonomy: fifteen side-effect classes against Section 9.2's fifteen action categories, four idempotency classes against Section 8.4's four crash-recovery bullets. - Rekeyed Section 9.2's matrix on
SideEffectClassinstead of a prose "action category" with no referent, and added a Condition column so the three cells holding non-enum decision strings resolve: "Allow with restrictions" and "Allow only in sandbox" become a guardedALLOW, "Deny initially" becomesDENYin thedefaultprofile. No outcome in the matrix changed. - Made the evaluator a pure function —
evaluate_deterministic(action, principal, run, ruleset)— with time passed in asevaluated_atand never read from a clock, so the same inputs produce the same decision on replay. - Established restrictiveness as a total order combined by
max:ALLOW<ALLOW_WITH_MODIFICATIONS<REQUIRE_APPROVAL<DENY. Section 9.1's "more restrictive wins" was undefined across four decision types; it is now a computation. Hardline is not a rank in that order but a short-circuit. - Located the hardline rules that Section 9.3 required but never placed:
src/agent_core/policy/hardline.yaml, packaged, frozen at import, and deliberately not behind a port — a port implies substitution, and these are the rules that must not be substitutable. Every rule carries a mandatorynear_missit must permit, so an over-broad pattern fails its own test. - Defined
policy_version, whichContextPlanalready consumed and nothing produced, as a content hash of the profile plus the hardline file (default@3f2a1c9d4e5b+h7c1e0a92), making rules version-controlled files frozen per process rather than rows in a table. - Generalized approval beyond tool calls with
ActionKind(tool call, memory write, skill authoring, artifact export), sinceapprovals.tool_invocation_idbeingNOT NULLmade the other three structurally unapprovable. - Gave
approvalsthetenant_idandprincipal_idthat Milestone 4's cross-tenant rejection criterion requires, three indexes, a unique index for one open approval per action, and a guarded resolution that is first-writer-wins: a second caller agreeing gets 200, a second caller disagreeing gets 409, and a cross-tenant caller gets not-found rather than forbidden. - Specified the shape of "denial becomes a structured tool result" as a field allowlist enforced by test, with a repeated-denial circuit breaker at three, so a denial teaches the model to stop without teaching it what to evade.
- Mapped Section 22's three trust tiers onto Section 11.2's seven trust labels,
with only
PLATFORM,TRUSTED_CONFIGURATION, andUSERable to authorize. - Added
GET /v1/approvalsandGET /v1/approvals/{id}, without which Section 17'sagent approval listhad no endpoint to call. - Added ten hard gates, six tracked metrics, and a twelve-step build order to Milestone 4, keeping the advisory layer sequenced after Milestone 6.
- Wrote ADR-0006 as already amended by ADR-0007, so the distinction between "never persist reasoning" and "may hold provider-opaque continuation in the checkpoint for the life of a tool loop" lives in the record rather than in one trailing sentence of Section 11.4.
- No product implementation was performed.
2026-07-24 — Event log and persistence spec¶
- Added
docs/plan/event-log-and-persistence.md, the persistence design for Sections 6.8, 6.9, 12.2, 14, and 15 and Milestone 2: the append transaction, projections, checkpoints, and the run queue. Recorded as ADR-0003 (amended for payload versioning) and ADR-0004. - Stated the layer's contract as observation, not durability — a committed event no projection ever observed is, to every consumer, an event that did not happen — and wrote the hard gates against that definition.
- Identified a silent missing-write hazard: two appends take sequences 5 and
6, the transaction holding 6 commits first, and a projection polling in that
window advances past 5 and never sees it. The log stays consistent,
UNIQUEis satisfied, and every rebuild reproduces the loss identically. - Resolved it by establishing that Section 27.5's one active run per session is
load-bearing for projection correctness, not only for contention, enforced by
a partial unique index, with snapshot-aware watermarking
(
pg_snapshot_xmin) specified as the companion change required if that default is ever relaxed. - Made sequence gaps legal: a rolled-back append burns its number, so consumers read after a watermark and never wait for a specific next sequence.
- Established
LISTEN/NOTIFYas a latency hint, never a delivery guarantee — it is transactional, so no outbox is needed, and at-most-once, so every consumer polls from a watermark first. - Added
events.payload_schema_version, required by Section 6.8 and Milestone 2 but absent from Section 15, together with pure, total upcasters that may never invent a value, and made an unknown higher version a hard error. - Gave projections four properties — deterministic, watermarked, rebuildable, never authoritative — with state and cursor written in one transaction, and gave derived events deterministic derivation keys so rebuilds converge instead of multiplying their own output.
- Made checkpoints deltas against periodic full snapshots with the conversation stored as event references, and made losing checkpoints costs time, not information an executable test.
- Added claim priority classes (interactive, async, maintenance) with capacity reserved per class rather than aging, which would make latency depend on queue history.
- Made every worker write fenced by
lease_epoch, since lease expiry is a guess: a zero-row update means stop, not retry, andheartbeatreturnsFalsewhen fenced rather than raising. - Specified queue-level retry that the plan lacked entirely — only lease expiry
requeues,
max_attemptsis 3,runs.failureis the dead letter — and added theidempotency_keystable that Section 16 and Milestone 2 both assume. - Added seven hard gates and five tracked metrics to Milestone 2, four ports
(
CheckpointRepository,ProjectionCursor,Projection,RunQueue), and four event types. - Created
docs/status/questions-for-review.mdrecording every decision taken without review during the plan-completion run, with its reasoning, alternative, and reversal cost. - No product implementation was performed.
2026-07-24 — Context engine spec¶
- Added
docs/plan/context-engine.md, the assembly design for Section 11 and Milestone 7: the cache boundary, the budget allocator, compaction, trust rendering, and the working-state lifecycle. Recorded as ADR-0020. - Split context into two regions with one membership rule — if a value can differ between two requests in the same session, it is not in the prefix. Membership is a property of item type, declared in code and asserted at assembly, so the current date cannot reach the prefix by looking like configuration.
- Made prompt stability enforced rather than assumed:
prefix_sha256is recorded on every request, and a scripted fifty-turn session crossing midnight with a revoked tool, corrected memory, and a forced compaction must yield exactly one hash. Added prefix epochs for changes that cannot be absorbed, with epochs-per-session tracked against a target of 1.0. - Pinned the tool set at session open and moved revocation to call-time policy denial, so a permission change does not rewrite the prefix or leak into cache timing.
- Gave
ContextBudgeta sizing rule: only history scales with the context window; every other class is capped absolutely, because prefix content is attention paid on every request. The prefix never yields — a class over its ceiling fails the session at open rather than truncating the system prompt. - Fixed the yield order under pressure as in-turn recall, then tool-result truncation to typed pointers, then compaction, and made tool call/result pairs atomic budget units.
- Separated purity from compression:
build()is a pure function and compaction is a checkpoint write, which is what makes retries safe and the byte-stability gate meaningful. - Established that untrusted content is elided, never paraphrased — summarization is a trust-label laundering vector — with typed pointers retaining label, size, and reference, and a summary-depth cap of 2.
- Added the nonced trust envelope with delimiter escaping, and the typed
context.update_working_statecontrol tool with per-field carry rules across turn boundaries, bounded lists, and constraints that never evict. - Handed
established_factsto memory formation as candidates subject to every eligibility gate, giving the write path a second input rather than a bypass. - Added five hard gates (determinism, prefix stability, budget conformance, tool-pair integrity, trust preservation) and four tracked metrics to Milestone 7.
- No product implementation was performed.
2026-07-24 — Session snapshot budget decided¶
- Closed the last retrieval open question. The session-open snapshot is capped by item count first and tokens second, never by a pure percentage of the context window: dilution tracks the absolute number of irrelevant items, so a larger window is not a reason for a larger snapshot. The percentage survives only as a ceiling.
- Set the starting
corebudgets: 40 items / 1,500 tokens for interactive sessions, 80 / 3,000 for long-running async runs that amortize one block over many requests, and 15 / 500 for child runs — each bounded by 2% of the model's window. - Reserved roughly two-thirds of the item budget for durable user-model and preference beliefs and the remainder for the opening-goal priming set, so project-specific beliefs cannot evict the "who am I talking to" layer the snapshot exists to carry.
- Made the number self-correcting rather than fixed: snapshot size should be
inversely proportional to retrieval quality and is expected to shrink as the
query former and ranker improve. Tuning is driven by two signals already present in
RecallTrace— snapshot utilization (shrink below about a quarter) and snapshot misses (grow when in-turn recall keeps re-fetching snapshot-eligible beliefs) — which pull in opposite directions by design. - Added the
Sizing the snapshotsection to the retrieval spec, recorded as ADR-0019 decision 17, and rejected three alternatives: percentage-of-window sizing, one budget for every session type, and growing the snapshot as memory accumulates. - Both memory specs now carry no open questions; the temporal entity graph remains unspecified.
- No product implementation was performed.
2026-07-24 — The recall trace becomes a user-inspectable surface¶
- Resolved the second retrieval open question: the
RecallTracehas two consumers — the operator tuning ranking and the user asking why the agent said what it said — and both read the same record. Two logs would drift, and the one shown to the user is the one that must not be wrong. - Specified that the trace is recorded in the render pass, never reconstructed,
and bound to the exact rendered bytes by
rendered_sha256. Re-running retrieval later returns a different set; a plausible reconstruction of a turn that never happened is worse than no answer. - Defined what a trace may honestly claim: what was in context, with cited beliefs marked used and the rest available. It never claims what the model attended to.
- Added the user-safe projection (
RecallTraceView/TracedBelief), which carries the statement, when and where it was learned, authority and source episode, confidence band, and citation, and excludes arm latencies, scores, candidate ids, and policy internals — dropped and blocked items are reported as counts only. - Sensitivity is filtered by the minimum of the recall surface's and the viewing surface's ceiling, and retention is two-tier over one record: operator fields expire on the tuning window, user-safe fields live and die with their session.
- Added a
TraceStoreport, three failure modes (trace disagrees with what the model saw, trace as a disclosure path, a rejected belief returning after re-derivation), and two hard-gate evals: trace faithfulness and correction durability. - Made rejection from a trace a typed, first-class formation input: not true
(retire), was true and has changed (supersede), true elsewhere but not here (lower
portability and record a negative scope override), and unspecified (flag and
down-weight, never retire). Added
BeliefRejectionand theMemoryStoremethodsrejectandoutstanding_rejections. - Established that rejections are events that re-derivation replays, matched by content rather than belief id since re-derivation mints new ids, and that rejecting is not deleting — a deletion keeps only a content-hash tombstone.
- Recorded as ADR-0019 decisions 15 and 16 and ADR-0018 decision 15, with build sequences updated in both specs so the trace is written faithfully from the first commit and rejections exist before re-derivation can violate them.
- No product implementation was performed.
2026-07-24 — Beliefs carry across projects¶
- Resolved the cross-project open question: beliefs carry from project to project so the agent learns from every project and environment it works in. Scope is split into isolation boundaries (tenant, principal, sensitivity — hard SQL predicates, unchanged) and relevance boundaries (project — a ranking and rendering input).
- Added a portability class per belief (
portable/contextual/local), bounded bybelief_typeat formation and lowerable but never raisable by the extractor; carried beliefs render with their origin project and at a reduced confidence band, and explicit local overrides outrank them. - Added promotion by cross-project corroboration to the formation spec: a belief
independently observed in two or more project scopes promotes to
userscope, retains every contributing origin, and emitsmemory.promoted. Recorded as ADR-0018 decision 14 and ADR-0019 decision 5. - Added the false transfer failure mode with its defenses, and paired transfer-precision / transfer-lift evaluation metrics.
- Expanded the two remaining retrieval open questions with their tradeoffs: the session snapshot budget is attention-bound rather than cost-bound and should be an absolute token cap rather than a pure percentage; user-visible retrieval traces are restated in terms of the commitments they impose now (a user-safe projection, retention, the sensitivity ceiling, and a user-rejection input into formation).
- No product implementation was performed.
2026-07-24 — Memory retrieval & ranking spec¶
- Added
docs/plan/memory-retrieval-and-ranking.md, the read-path design for long-term memory: the three retrieval moments forced by the prompt-stability invariant (frozen session snapshot, in-turn recall, child-run recall), query formation from working state, the hard scope filter, multi-arm recall fused by reciprocal rank, the explicit ranking function, supersession collapse, the safety pass, byte-stable rendering, retrieval traces, and the usage-feedback loop back into formation. Recorded as ADR-0019. - Wired the spec and ADR-0019 into the MkDocs navigation and the ADR index, and added read-path pointers from Milestone 9 and the formation spec.
- Three open questions are recorded for decision: the session snapshot token budget, whether project-scoped beliefs may surface cross-project, and whether retrieval traces become a user-facing surface.
- No product implementation was performed.
2026-07-24 — Memory formation & consolidation spec¶
- Added
docs/plan/memory-formation-and-consolidation.md, the detailed write-path design for long-term memory (formation pipeline, conflict resolution with bi-temporal supersession, data model, governance, evaluation, and build sequence). Recorded as ADR-0018. - Wired the spec into the MkDocs navigation and the ADR index, and added a pointer from Milestone 9 in the engineering plan.
- Resolved two design decisions in the spec and ADR-0018: memory formation is fully autonomous from the start (safety via deterministic eligibility gates, the untrusted-content write ban, and after-the-fact review), and the builtin consolidation path is built to parity before any external provider.
- Resolved the remaining formation questions: a tiered memory model (a continuous confidence lifecycle plus an explicit working/episodic/semantic/archival hierarchy), the user model is a projection over user-scoped beliefs, and re-derivation is opt-in per principal.
- No product implementation was performed.
2026-07-20 — Documentation system established¶
- Archived the source Word document to
archive/Modular_General_Purpose_AI_Agent_Engineering_Plan.docxand recorded its SHA-256 checksum inarchive/README.md. Preserved the prior Word revisions (v1.0 through v2.3) underarchive/versions/; the canonical archived document is a copy of v2.3. - Converted the complete engineering plan to canonical Markdown at
docs/plan/engineering-plan.md(Pandocdocx→gfm, then deterministic cleanup: single level-one title, fenced code blocks with language hints, normalized tables, and removal of the static Word table of contents and title-page artifacts). - Relocated three security controls — non-bypassable hardline rules, tiered
credential scrubbing with fail-closed passthrough, and default-deny pairing for
untrusted inbound surfaces — from the "Revision summary" list to their correct
home in Section 22, "Security baseline". No requirement text was changed; only
placement was corrected. The archived
.docxretains the original placement. - Created coding-agent instruction files:
AGENTS.md,CLAUDE.md, and.github/copilot-instructions.md. - Created machine-readable project state at
docs/status/project-state.yaml(current milestone: 0) and a concisedocs/plan/current-milestone.md. - Wired the existing
docs/adr/records (ADR-0007 through ADR-0017) into the documentation site. - Created the documentation build system: the MkDocs site (
mkdocs.yml), a single-file HTML build (docs-manifest.yaml,scripts/build_docs.py), documentation validation (scripts/check_docs.py),Makefiletargets, and a CI workflow (.github/workflows/docs.yml).
No product implementation was performed. Milestone 0 has not been started.