Skip to content

AgentHydra 0.39.0

Choose a tag to compare

@github-actions github-actions released this 06 Sep 23:51
· 64 commits to main since this release

Added

  • A permanent record for every chat, which outlives the chat (server/src/db.ts
    session_stats, server/src/analytics.ts). Claude Code deletes transcripts on its own schedule
    (cleanupPeriodDays, 30 days by default) and the scan cache was pruned along with them, so a
    conversation's entire history (tokens, cost, how long it ran, which account paid for it)
    evaporated a month after it finished, and the Analytics tab's "all time" quietly meant "the last
    month". Measured on the machine this was written for: 35,943 transcripts on disk totalling 233.5B
    tokens, with nothing older than 30 June, against a year of real use. Every scan now writes a row
    that is NEVER pruned: tokens by kind, cost, first/last activity, turns, engaged time, tool calls
    and errors, compactions, files touched, lines added and removed, size, and the instance that ran
    it. When a transcript disappears the row is stamped gone_at instead of deleted, and the spend
    report folds those rows back in, so a total stops shrinking when a file is cleaned up.

  • Money or tokens, across the whole Analytics tab (web/src/components/AnalyticsView.vue,
    composables/useAnalyticsPrefs.ts). Several panels could only ever answer in dollars, so "how
    many tokens went where" was unanswerable on a tab named Analytics. One switch changes the unit
    everywhere; per-day, per-project and per-account token splits were added server-side to make that
    possible. A panel with no token figures says so rather than drawing an empty chart, because blank
    bars and "you spent nothing" look identical.

  • A calendar grain for "when the work happens" (web/src/components/charts/CalendarGrid.vue).
    The hour-of-week heatmap collapses every date into a 7x24 rhythm and throws the calendar away, so
    it could not answer "which weeks was I actually working" over a window of months. One square per
    day, months across the top; the hour grid is one click away.

  • Keep the 5-hour window running (server/src/session-keepalive.ts). The rolling 5-hour quota
    window only starts when an account is used, so an idle account makes you wait the full five hours
    from the moment you need it. Opt-in, OFF by default, and it declines on its own terms: an account
    whose window is already running is skipped, one at or above a weekly-percentage floor is skipped,
    and an unreadable quota reading is a skip rather than a guess. Not a headless chat: it is the
    same shape as the /usage probe that headless-policy.ts explicitly carves out, being one
    throwaway prompt, the answer read, the transcript deleted.

  • Click an account, get its address (Instances tab, Codex table and the quick-instances window).
    The column shows the email handle because that is what fits; clicking copies the full address,
    which is the only form that identifies a login outside the app.

  • Which account a chat is talking to, on the open transcript (useSessionAccount.ts). A chip
    under the title, and an account section at the top of the three-dot menu with "Open this account"
    and "Copy the account address". Every instance runs a different login and the transcript pane
    named none of them.

Changed

  • Instance rows follow the login, not the profile's display name
    (web/src/lib/instance-appearance.ts). The Anthropic profile's full_name is whatever someone
    typed into claude.ai, so a fleet named that way read "Toby", "Martin", "Michael Griswold":
    friendly words that do not say which login each row is. Rows now use the email handle, and a
    stored name that no longer matches the account it is signed into is flagged, with a one-click
    "Name it after the account" in the three-dot menu.

  • A time period on Analytics now means what it says (server/src/analytics.ts windowShare).
    The window was applied per SESSION on last_ts, so a session whose last turn landed inside it
    contributed its ENTIRE life and one that ended a day earlier contributed nothing, not even the
    part that was inside. Every panel now takes the same day-proportioned slice, so the headline, the
    per-model split and the day chart cannot disagree. A sub-day window is answered at day
    granularity, which is the resolution a stored row actually has.

  • The settings dot explains itself. Clicking the header's gear while an update is waiting
    scrolls to the Updates card and pulses it, and the dot goes quiet for the rest of the session,
    returning on the next launch because the update is still there.

  • The Focus button carries the running dot the status column already shows, on both the Claude
    and Codex tables.

Fixed

  • default and other are refused as instance names (server/src/core/lifecycle.ts). Both are
    words the app already uses to mean something other than a folder: default is the regular
    non-isolated Claude Desktop, other is the sessions filter's no-instance scope. An instance
    taking either could not be told apart from it, and nothing failed loudly: a chat in a folder
    called default was indistinguishable from one in the real default install.

  • The regular Claude Desktop is identifiable (CMInstance.isDefault). A session records its
    instance as a folder name or the literal default, and nothing on the instance row could match
    the second, so the app had no honest way to name that account. One shared helper now answers "is
    this the regular install?" for the row, the quit guard and the delete guard, which each kept their
    own copy, and it folds case only on Windows, matching normalizePath. The old copies folded it
    everywhere, which on Linux and macOS made a differently-cased folder look like the default
    profile.

Added

  • The courier lane, entered here late (orchestrator/scripts/courier.py, stage_reply.py,
    lib/deliverylib.py; landed 2026-09-03 with the orchestrator's move back into this repo, after
    v0.38.3 was tagged, and left out of this file at the time). It is the last manual lane closed:
    an AI stages a decided reply, and the courier delivers it into the chat through the app's own
    composer (or the peer channel for a live session), proving the chat moved afterwards. Rails in
    order: held? breaker? resolves to exactly one chat? never mid-turn on the composer route? the
    pane shows a snippet of this chat's own last words? Then send, then confirm. sweep.py --deliver
    runs it as part of an acting pass.

  • A provenance ratchet on the agent catalog (scripts/checks/catalog-row-provenance.mjs, wired
    into CI). The 58 rows in server/src/agent-catalog.ts say where each coding agent keeps its
    conversations, and their paths were compiled from a third-party registry rather than read from
    each tool's own source. A wrong path there is invisible by construction: a row pointing at a
    directory that does not exist produces exactly what a correct row produces on a machine where
    that tool is not installed, so it can never be told from "not installed" and lives forever. Three
    rows were checked against upstream source on 2026-09-04 and all three were wrong - Hermes Agent
    and OpenClaw both pointed at directories their projects do not have (fixed), and aider carried
    an empty dirs, unmatched on any machine without AIDER_DIR set (given ~/.aider, which its
    own main.py writes to). Rows now carry an optional verified: '<repo> <file> (<date>)', and
    the guardrail ratchets on the count so verification can only grow, fails a marker that names no
    file or date, fails a row that cannot match at all, and fails if its own parser reads fewer rows
    than the table declares.

  • orchestrator/scripts/lib/incidentlib.py - THE INCIDENT LEDGER, ported from
    NousResearch/hermes-agent's cron/incidents.py (MIT) and adapted to this toolbox's
    JSON-rows-in-state style (no SQLite). Groups repeated failures by a normalized cause
    signature instead of leaving them as anonymous rows in the attempt ledger: the same chat
    failing the same way, or several unrelated chats failing for one shared reason, collapses
    into ONE incident (lifecycle open -> acked -> resolved) with a repeat count and last
    error. ledgerlib.note()/annotate() now file an incident for every deterministic or
    explicitly-flagged failure and stamp the ledger row with the incident id, so the two can be
    joined. sweep.py gained a SHARED-CAUSE BREAKER: 3+ consecutive same-signature failures in
    one lane halt the rest of that lane for the pass (--breaker-threshold to tune) instead of
    repeating a cause that will not clear chat by chat, and file one incident naming every chat
    left behind. New python orch.py incidents (list open/acked, --ack/--resolve, --all)
    surfaces it; the dashboard's /data/incidents route and /data/suppressed's
    incidentsOpen count expose it there too.

  • Orchestrator: the unblock lane now classifies a stuck permission prompt before pressing
    it, tri-state (APPROVE / DENY / ESCALATE)
    - our own classifier; the tri-state idea came from
    reading NousResearch/hermes-agent's approval.py (MIT), none of its code. Previously unblock_prompts.py pressed Allow on any chat whose
    configured mode was bypassPermissions, whatever the pending command actually was.
    It now also classifies the command against orchestrator/scripts/lib/approvallib.py's
    policy (state/approval_policy.json, created with a WHY-comment on first run, hand-edited
    only - never inferred from a chat's own transcript text): hardline-destructive commands
    (rm -rf, a shared-branch hard reset, a credential path, ...) DENY and are recorded, never
    pressed; clearly safe ones (read-only inspection, build, typecheck, test, lint, git
    status/log/diff) APPROVE exactly as before; everything else ESCALATEs into a new judgment
    queue (state/approval_escalations.json) that interview.py --ask now also surfaces, so a
    person or the /orchestrate AI decides instead of it being pressed on a guess. The
    scheduled UNATTENDED run presses only APPROVE; an INTERACTIVE run (--force, a person at
    orch.py) may also press an ESCALATE row, after the command has been shown.

  • A mutation ledger with undo, for every act the orchestrator performs on a Desktop chat
    (a plain before/after ledger; unrelated to any external checkpoint tooling). Until now archive_chat.py, rename_chat.py, migrate_chat.py,
    hold_chat.py and compact_chat.py left no before-image of what they touched, so a wrong
    archive or rename (the orchestrator's README documents 6 of 29 chats archived wrongly in one
    day under v2) could not be undone from here - only by hand, on a screen, from memory. Each of
    those five scripts now writes down what its target looked like immediately before it acted
    and immediately after (orchestrator/scripts/lib/mutationlib.py, same locked-JSON discipline
    as the attempt ledger and the holds file); python orch.py mutations lists every entry newest
    first with an undoable flag, and python orch.py undo <id> reverses one through the exact
    same rail-guarded script that performed it (unarchive, rename back, migrate back to the source
    instance, or release/re-hold), verified by that script's own fresh mutation row rather than
    trusted on exit code alone. Compaction is recorded but never undoable - it is lossy by design,
    so no inverse exists, and the reason is stated rather than guessed.

  • Startup-liveness watchdog so a daemon that hangs during boot (a locked sqlite file, a port
    probe that never returns, an updater step that stalls) crashes and gets restarted instead of
    sitting there indistinguishable from a slow one - tray icon idle, nothing logged, until someone
    restarts it by hand an hour later. Armed at process entry (server/src/main.ts, before importing
    the daemon or --instances entrypoint), renewed at each boot phase as it's reached (db open,
    migrations, scheduler start, queue recovery, listen), and disarmed the moment the port is actually
    bound. If the deadline elapses with no renewal, it logs the last-known phase and pid to both
    stderr and daemon.log, then exits with a distinct code (87) so the tray/service supervisor
    restarts it rather than a silent hang. Deadline is AGENTHYDRA_BOOT_DEADLINE_MS, generous by
    default (120s full daemon, 30s --instances); inert under bun test. Idea ported in shape from
    NousResearch/hermes-agent's startup watchdog (MIT) - see server/src/boot-watchdog.ts.

  • Hermes Agent joins the readable session sources (PLAN.md's DEFERRED list named it first: 241k
    GitHub stars, larger than every other deferred source combined). Hermes keeps everything in one
    SQLite file, state.db, at the root of HERMES_HOME (~/.hermes on POSIX, %LOCALAPPDATA%\hermes
    on native Windows), plus a separate state.db per named profile under profiles/<name>/. AgentHydra
    now lists, tails, exports and body-searches Hermes sessions the same way it already does OpenCode's
    shared SQLite store, with a profile standing in as the "project" grouping. Usage is priced through
    AgentHydra's own catalog by model name rather than trusting Hermes' own cost columns, so a model the
    catalog has no price for costs $0 and is flagged unpriced instead of silently taken on faith. Purely
    a reader: the queue, composer and resume-in-terminal stay Claude-only, and nothing here writes to a
    Hermes store. New: server/src/hermes-sessions.ts.

  • Failed queue runs are now grouped into incidents, so twenty overnight runs failing the same
    way read as one problem instead of twenty.
    Ported from NousResearch/hermes-agent's cron
    incident tracker (MIT). A failed run (via finalize() or a pre-launch refusal in
    dispatch.ts) is recorded against (scope, key, error signature) - the signature survives
    timestamps, pids, and paths changing between runs of the same project, so a repeat only bumps a
    counter rather than minting a new alert. Lifecycle is open -> acked -> resolved; a resolved
    incident whose error recurs reopens rather than staying silently closed, and a genuinely
    different error on the same project opens a new one. Desktop/email notifications (reusing the
    existing reset-notification channels) fire on the first occurrence and on a reopen, and are
    suppressed for every repeat in between - the count still increments. The one pre-launch refusal
    that is permanent by policy (headless dispatch is currently disabled outright) is excluded from
    incident tracking entirely, so it cannot page on every dispatch attempt. New: server/src/incidents.ts
    (the model), an incidents table (server/src/db.ts), GET /api/incidents, POST /api/incidents/:id/ack, POST /api/incidents/:id/resolve (server/src/routes/incidents.ts),
    MCP tools list_incidents / ack_incident / resolve_incident, and a collapsed "Incidents"
    panel above the run queue with an open-count badge and ack/resolve buttons
    (web/src/components/IncidentsPanel.vue).

  • "Never claim an act landed without checking" - now enforced, not just documented. This
    repo's own orchestrator rule 4, finally applied to the run queue as well. The never-retry-on-
    UNKNOWN half (a provably-never-attempted row may be re-queued; one whose outcome is unknown
    never is) is a discipline NousResearch/hermes-agent's cron/delivery_queue.py documents for its
    own queue; reading it prompted this, no code is shared. Two halves:

    • The run queue. A finished run's exit 0 no longer means completed by itself: the
      daemon now re-reads the run's own transcript and requires an assistant turn timestamped
      after the run started. Missing that, the run reads unverified - a new, distinct queue
      status shown everywhere completed/failed/etc. already are (QueueItemCard, the run
      viewer, list_queue/get_run_events), logged at WARN with what was missing, and never
      silently delivered to a migrated run's desktop target. A run whose failure is genuinely
      UNKNOWN (the process/pid vanished with no exit code - not a real code claude reported) is
      recorded as such and is never auto-retried without saying so.
    • The orchestrator's ledger. ledgerlib.verify(kind, session_id, verified) attaches a
      true/false/None read-back verdict to an attempt row (unverified() surfaces the
      false/unknown ones for the judgment queue). archive_chat.py, rename_chat.py and
      migrate_chat.py - which already re-read the target's state after acting - now record that
      verdict on the ledger instead of only reporting it; rename_chat.py also had a real bug this
      closed, where a failed verify READ silently collapsed to the same outcome as a verify that
      succeeded and disagreed (unknown was reading as false).
  • THE ORCHESTRATOR IS BACK IN THIS REPO - as a folder, not a rewrite (owner order, Michael,
    2026-09-03: "migrate the orchestrator into AgentHydra so I don't have to explain that you have
    to use both"). orchestrator/ is the v3 Python toolbox exactly as it stood in its own repo
    (Lunarwerx/orchestrator, now an archive): orch.py, scripts/, its 647 unit tests, and its
    remote front-end (orchestrator/server + orchestrator/web, now root workspaces). The
    2026-08-31 boundary survives - the scripts still only ever talk to the daemon over HTTP - but
    there is one surface: the daemon runs them (server/src/orchestrator.ts; GET /api/orchestrator, POST /api/orchestrator/run) and the MCP server exposes
    orchestrator_menu, orchestrator_run, orchestrator_loop and orchestrator_switch. Script
    names are validated against the menu grammar and arguments travel as an argv array, never a
    shell. Nothing there acts without the tray icon, as before. The retired v1/v2 reference trees
    (old/) and the duplicate src/ + tests/ copies did not come along: they stay reachable in
    the archived repo, and the README's postmortem paragraphs carry the lessons. Release bundles
    stage the python half beside the executable (orchestrator/), where the compiled daemon looks
    for it; the spawn forces UTF-8 and normalises CRLF so the second machine reads the same bytes.
    A machine that ran the standalone checkout has a written one-time cut-over (scheduled tasks,
    tray shortcut, state/) in orchestrator/README.md. The /orchestrate command that 0.37.0
    removed is back (canonical copy in .claude/commands/, beside /hydra-status), rewritten onto
    the four MCP tools instead of a path to a second repo.

  • Redeem a banked Codex reset credit from the Instances view. A ChatGPT-account Codex login
    can bank /usage reset credits, each restoring the FULL 5h + weekly rate-limit windows in one
    shot - previously AgentHydra could only read the count (rate_limit_reset_credits), never spend
    one. The Codex row's menu now has "Redeem reset credit" (server/src/core/codex-account.ts's
    redeemCodexResetCredit, POST /api/codex-instances/:id/redeem-reset-credit, MCP's
    redeem_codex_reset_credit), guarded the way the Codex CLI's own picker is: it refuses unless
    the busiest window is already fully used (100%) or the caller passes force, since redeeming
    early wastes most of a credit's value. The button disables itself with the reason when the
    already-cached quota chip shows the guard would refuse. Ported from
    NousResearch/hermes-agent's account_usage.py (MIT, Copyright (c) Nous Research); the access
    token is read into a local binding only and never logged or returned.

Fixed

  • Every closure of the 42-item audit was re-checked against its own acceptance criteria, and 12
    of them were overstated
    (2026-09-06). Two independent reviewers read the committed code and the
    named regression test for each item, with a third deciding wherever they disagreed. 30 held. The
    suite had been green throughout and said nothing about the other 12, which is the point: a green
    suite proves the tests that exist pass, not that any of them asserts what the item required.

    Three were real defects rather than overclaims, and all three were the same shape, a fix applied
    at one call site while the finding named a class of sites:

    • The transactional installer destroyed the data it exists to protect. It moved
      orchestrator/state/, the scheduler's ledger, out of the rollback copy before the step that can
      fail, and the cleanup deletes the staging directory unconditionally, so any mid-install failure
      rolled back a toolbox whose ledger had gone with it. It is copied now, and the rollback copy is
      discarded only once everything has landed. The rollback test that existed could not have caught
      it: it injected its failure one component too early. The new one was confirmed to fail against
      the old code.
    • The instances window accepted any loopback origin. The exact-origin allowlist landed on the
      daemon and not on the second local server, which serves instance create, open and quit, so a page
      on any other localhost port could drive it from a browser. Both build the allowlist from one
      module now, and a new guardrail fails the build if a local server ever binds a port without it.
    • Cancelling a run on Linux or macOS killed one process, not the tree, under a comment
      promising otherwise, so everything the agent had spawned kept running. There is one tree-kill
      implementation now instead of two, of which only one had been fixed.

    The rest were coverage gaps, each now closed: session search read only the default database for
    OpenCode-format stores (Kilo, MiMo Code and IcodeMate sessions listed and opened but could not be
    found), and its index keyed without the store, so two stores sharing a session id overwrote each
    other; the incidents panel still rendered "no incidents" when the read had actually failed; the
    updater's own apply path had no test at all behind its extracted helpers, and the daemon could not
    report which component versions had landed; the toolbox menu never showed which actions the tray
    icon gates and which run directly; the release smoke test never asked the booted binary about its
    toolbox; and the tunnel-readiness fix and the kit-sync hook were both unguarded. The full linter
    now runs clean, and the one dead translation key is gone.

  • Linux enumeration no longer depends on ps/pgrep, and two identity keys follow the store
    (2026-09-05, found by running the repo's own ubuntu CI leg in its container after the audit
    closed; GitHub's runner ships procps and had hidden all of it). On Linux the process table is
    read from /proc for Claude and Codex discovery alike, and the orchestrator's tree kill walks
    /proc too. In the CI image, which has neither ps nor pgrep, every Unix enumeration used to
    answer "could not look": the delete guard rightly refused every instance removal, and a
    timed-out toolbox run killed only the interpreter while its grandchild held the pipes and the
    route never answered. The drain after a kill is now bounded as well: a pipe still open five
    seconds after the tree kill is abandoned with what arrived. Done-marks and subagent ownership
    key on the session's store (database path for OpenCode-format and Hermes stores), so two Hermes
    profiles or two OpenCode-format databases sharing a session id no longer share a mark or steal
    each other's children; marks set under the old key are still found. And Settings' scheduler
    panel now says plainly that this build cannot dispatch unattended runs, instead of offering a
    switch that does nothing.

  • The last twelve audit findings (audit AH-07/08/09/10/11/12/16/25/30/35/40/42, 2026-09-05),
    which closes the 42-item orchestrator audit. A compiled update now brings orchestrator/ and
    misc/ to the release's exact content as one unit with the executable: the toolbox is swapped
    with its state/ carried across and a .release-version stamp, retired files go, a failure
    rolls every part back, a bare-executable install acquires the toolbox, and the swap refuses to
    run while a toolbox script is executing through the daemon. The manual install.ps1 is
    transactional the same way (stage, canary the staged exe, refuse under a running instance,
    rename-aside per component, restore on any failure) and a test pins its component list to the
    updater's. A long toolbox run is a durable operation: an idempotency key makes a retry after a
    dropped connection return the original outcome rather than start a second act, async returns
    an id to poll, and a running operation can be cancelled (its whole process tree is killed).
    MCP requests over stdio dispatch concurrently with per-request cancellation, so a pause or
    status call is no longer stuck behind a 30-minute act. The daemon's guard and CORS accept an
    exact origin allowlist instead of any loopback port. The web no longer offers queue creation or
    Run controls that the server refuses unconditionally: the composer sends through the working
    delivery path and shows the server's real reason, and the README describes what works. Every
    toolbox script carries one explicit capability contract (kind, invocation, guard, availability)
    that the menu, --catalog and the arm check all read, replacing classification by docstring
    prose. Scheduled lanes hold a proof-of-death job lock (owner pid + creation time, heartbeat,
    reclaim only when the owner is provably gone) instead of an age-only directory. The doctrine
    picker's confirmation ledger is written under a lock, so a concurrent drop can no longer erase
    a fresh confirmation. Fleet-wide enumeration pages the session list to the end and raises on a
    bad page instead of treating the first 500 rows as the fleet. Sessions carry a locator that
    includes the store they came from, so Kilo, MiMo Code, IcodeMate and multi-profile Hermes rows
    stop colliding. And a collection guard fails the Python suite when any test_*.py collects as
    zero cases; it fired at once on the chatwatch checks, which had been a bare script off the gate
    since they were written. Finally, the desktop-record mutation guard compares the record's BYTES
    before it writes, not its timestamp and size: a same-size rewrite by the app inside one
    filesystem timestamp tick was invisible to the old check and a stale copy went straight over it.

  • A damaged instance registry is never silently replaced, and a delete never claims more than it
    did
    (audit AH-01/02/03, 2026-09-05). Three related holes in how the CLI and Codex instance
    registries and the profile deletes handled failure, each reproduced against the real functions
    before it was closed. (1) Both registries read a malformed or unreadable cli-instances.json /
    codex-instances.json as an EMPTY store and their next write overwrote it, so one create after a
    corrupt read came back ok: true with a fresh file holding only the new record - every managed
    login identity in the old file gone, with a success message. Every read and write now goes
    through one core/json-store.ts: missing, corrupt and unreadable are told apart, every mutation
    refuses on a damaged file and leaves its bytes exactly as found, writes are temp-file + rename,
    and mutations hold an interprocess lock, because the quick-instance daemon writes the same files
    as the main daemon and last-writer-wins between two processes silently dropped whichever record
    landed first. Boot logs what the disk and the registry disagree about (a login dir no record
    claims, a record whose dir is gone) instead of leaving that to be discovered by hand. (2) The
    process scan behind the desktop-profile delete folded "could not enumerate" into "nothing
    running", so a transient PowerShell/CIM failure during a confirmed delete authorized removing a
    profile a running app was still writing into - its own fail-closed catch never fired because the
    scanner had swallowed the error first. The scan now says which of the two it is, and both the
    Claude and Codex deletes refuse on unknown. (3) A delete whose directory removal FAILED (a
    locked profile) dropped the registry record anyway and reported success, leaving the login on
    disk with no row to manage it from; the record now stays and the real error comes back.

  • Shared journals no longer lose each other's writes, stale UI responses are discarded, and the
    queue API cannot forge history
    (audit AH-13/17/18/21/22/29/31/37, 2026-09-05). Four journals
    the toolbox's lanes share - the workspace trust file, the chip handoff list, the standing
    manager's role claim, and every chat's metadata record - were each read whole, changed, and
    replaced with no lock between lanes, so the last writer silently discarded the other's change
    (reproduced for chips: a newly seen chip vanished and a dismissed one came back). Each now
    holds a named lock around read-modify-replace and re-checks the file's revision right before
    the replace; a record the desktop app rewrote underneath is re-read and re-applied, and one that
    keeps changing is left alone with an error rather than overwritten from a stale copy. In the
    browser, queue and scheduler refreshes and the transcript tail carry request generations, so an
    old slow response can no longer resurrect a deleted row or paint the previous filter's
    transcript under new controls; the remote app's "refresh every reading" now refreshes the Rules
    and Scripts it had loaded. The queue API splits what a person may edit (title, position, start
    time) from what only the runner may write (status, pids, timestamps, exit codes, import state):
    a client can no longer mark an item running or completed, edit an active row's identity, or
    delete a row still marked running. And the session export, which by contract renders one whole
    document, now refuses a transcript over 64 MB up front with a 413 that names both sizes and
    points at the raw download, instead of claiming to stream while holding it all.

  • An outage no longer looks like an empty account, and every failed action says why (audit
    AH-20/23/26, 2026-09-05). With every API read failing, the main app used to show "No sessions
    found", "Scheduler off" and an empty queue. Each resource now carries its own loading, error,
    stale and unavailable state: a first-load failure shows "unavailable" with a Retry, a later one
    keeps the last good data and marks it stale, and one resource's error never touches another.
    Queue run/cancel/delete and the composer's send now show the server's real error text instead
    of a bare count, and the prompt survives an unconfirmed send. The remote app retries a failed
    startup with a bounded backoff and a Retry button, tells an authentication refusal apart from an
    unreachable gateway (the former goes to login, never a reconnect loop), reports a failed
    sign-out, and shows a tunnel that died with its reason rather than as "off"; a login whose
    identity-provider discovery fails gets the gateway's retry page instead of a raw server error.

  • The toolbox's locks no longer crash under contention on Windows (found while landing the
    above, 2026-09-05). Every lane's lock - ledgers, holds, the delivery queue, the new journal
    locks, the naming pass - is an exclusive-create of a lock file, and both helpers tolerated only
    "already exists". On Windows the previous holder's unlink leaves the file's name pending-delete
    for a few microseconds, and a create landing in that window answers "permission denied"
    instead, so a lane that owed a wait crashed: measured 42 crashes in 1,800 contended
    acquisitions across six threads. The permission-denied answer is now treated as contention
    (wait, or for the non-blocking lock, defer), and a contention test pins it. The timed-out
    toolbox run on Linux and macOS now also kills the interpreter's descendants (an actuator it
    was blocking on), not just the interpreter; Windows already did through taskkill.

  • Updates are verified before they run, long scripts cannot exhaust the daemon, and a dead
    tunnel says so
    (audit AH-14/19/24/27/38/41, 2026-09-05). The compiled updater now checks a
    downloaded archive against the release's published SHA-256 manifest before extracting it or
    running anything out of it, and refuses a release that publishes no manifest; until now its
    only gate was running the download to see if it printed the right version, which is a
    compatibility check, not an integrity one. (What it proves: the bytes are the ones the release
    published. What it does not: who published them; a signed manifest is still open.) The
    orchestrator adapter bounds a script's output while reading it, keeping the last 200k
    characters of each stream and counting what it let go, instead of holding a runaway script's
    entire output in memory and trimming afterwards. The remote gateway now reports an outage when
    its tunnel connector dies after it was ready, clears the advertised URL, and cannot be talked
    back into "ready" by a late message from the dead connector. The local kit-drift check and the
    pre-commit guard cover both vendored kit targets, not just the main web root. Release smoke
    asserts the orchestrator payload is inside every archive and that test and runtime-state
    directories are not. And the README screenshot fixture matches the session DTO again, with a
    test that keeps it that way.

  • Fleet actions land once and read the right store (audit AH-04/05/06/33/34/36, 2026-09-05).
    Six more findings, each reproduced before it was closed. The Python toolbox now finds the daemon
    the way the MCP server does (explicit URL, explicit port, then the port the daemon ACTUALLY
    bound, then 7787) and the daemon pins its own bound URL into every toolbox child it spawns - a
    daemon that had hopped off a busy 7787 used to leave its toolbox talking to whatever answered
    there. Importing a session into a desktop app now holds one claim per session across every
    entry point (direct route, migration, batch, the message route's heal): a concurrent same-target
    request waits and coalesces onto the row the first one made instead of spawning a second import
    and a second identically named row, which made every later title-aimed action on that chat
    refuse as ambiguous. Automatic reply staging checks for an existing pending reply inside the
    same lock it appends under, so two lanes planning the same wake in one window produce one row.
    The quota budget's token count walks nested subagent and workflow transcripts, which the
    transcript index already counted as separate spend; a window in which the work was delegated
    used to report no activity at all. Kilo, MiMo Code and IcodeMate sessions are read from their
    own databases in tail, list metadata, export and analytics, not from the default OpenCode one
    (they listed fine and then opened as "transcript not found"). And the in-memory metadata cache
    treats a revision as mtime AND size, as the persisted cache always did, so an append that lands
    inside the same timestamp tick is re-parsed rather than served stale.

  • Delete and undo of one chat can no longer interleave, twin cleanup re-checks liveness at the
    moment it acts, and a failed self-update no longer erases edits made while it ran
    (audit
    AH-28/32/39, 2026-09-05). Three more places a decision was older than the act it authorized.
    delete_chat and its --undo now hold one per-chat lock for the whole transaction, because an
    undo that landed between the trash copy and the unlink loop was itself unlinked - both sides
    reported success and the chat was gone (reproduced with the production functions). Whichever
    arrives second now defers, refused-class exit 3, and a later undo restores everything.
    audit_twins --fix decided "not live" once per pass and then waited up to 60s for a window
    mutex and drove a 240s actuator on that stale answer; it now takes the same per-chat archive
    lock archive_chat holds, asks liveness and engine host again once the window is its own,
    defers a copy that went live meanwhile, and treats a daemon that cannot report liveness as
    unknown (refuse) rather than as an empty room. The shared source updater (kit
    updater-engine.mjs, synced) proved the tree clean BEFORE the pull and then, on a failed
    install/build minutes later, ran git reset --hard - deleting anything typed into the checkout
    meanwhile. It now re-reads the tree first: changes made during the update go into a named git
    stash before the reset and the message says so (git stash pop restores them); if the tree
    cannot be inspected or stashed, it does not reset and says the checkout needs a hand instead.

  • A compacted desktop chat no longer shows up as two or three chats (owner, Michael,
    2026-09-03: "I have a feeling compacted chats or something, become multiple entries"). He was
    right, and the mechanism is specific. The desktop app rolls a chat onto a new transcript id when
    it compacts, and it does so by REPLAYING the retained history into the new file before writing
    the compaction marker - so the marker the continuation detector looks for among a transcript's
    first records sits hundreds of records deep and was never found. Measured on one chat: three
    transcripts, the marker at record 1,501 of the newest, three rows under two titles. The app
    records every retired id in its own metadata (priorCliSessionIds); the session list now lays
    those links over the index (withDesktopContinuations in server/src/sessions.ts, fed by
    retiredSessionIds() in server/src/instance-sessions.ts) and folds them exactly as it folds a
    detected continuation, crediting the retired ids to the survivor so instance, archive and queue
    lookups keep working. A claim made only by an ARCHIVED tombstone counts too, because after a
    migration the tombstone left behind is the only record that still remembers the lineage. On this
    machine: 28 of 2,118 chats had rolled, 37 phantom rows.

  • The orchestrator's own tests stop depending on Claude Code being installed. Their first run
    on GitHub went red on four tests, and all four had one cause: the runner has no claude binary.
    compact_chat.main resolved the CLI before choosing its runner, so an injected test runner was
    never reached and the runner= seam only looked injectable - the executable is the real runner's
    dependency, and it is now resolved as one. The one test that genuinely asserts an installed
    binary skips loudly, naming the machine fact, and a new test pins the fallback so the
    bare-name branch is covered everywhere. These never surfaced before because the CI step that
    runs them arrived with the orchestrator and had never been pushed.

  • The release notes stop promising the two Windows downloads are otherwise identical. The
    orchestrator ships as a FOLDER beside the executable, so like the tray toolkit it can only ride
    in the zip - an .exe install answers GET /api/orchestrator with present:false and its four
    MCP tools do nothing. The asset table said "Everything else is identical", which turned a silent
    omission into a written promise; it now has an orchestrator column and says what the single file
    is. The table's own comment already made this rule for the tray icon ("costs real users, who
    reasonably read a missing icon as a bug"); it now states the general form, so the next payload
    staged beside the binary cannot become a third quiet exception.

  • The orchestrator's gate no longer reads a /compacted chat as "may be working" for ever. A
    chat that finished its turn and was then compacted ends on a <local-command-stdout>Compacted
    record - user-role, and no model ever answers it - and the idle test accepted only a completed
    assistant turn as the newest record, so nothing could move or archive such a chat until a person
    killed its engine by hand (two chats, 2026-09-03). Local plumbing a transcript ENDS on - a slash
    command the app answered itself, its printed output, the caveat banner, a compaction summary - is
    now stripped before the tail is judged (strip_local_tail in orchestrator/scripts/lib/gatelib.py).
    A slash command still awaiting the model, and anything in flight under an auto-compaction
    summary, still read as mid-turn.

  • A chat parked at a usage wall no longer reads as "may be working". An account out of
    budget until its reset cannot write, so its chat is stopped, waiting - the plainest case
    there is. But the wall arrives as an api_error record, which the gate's completed-turn test
    excludes, so such a chat was unmovable for as long as its engine lived, and moving it OFF the
    exhausted account is the one thing that would have helped. Quota walls only: a transient
    overload is one the engine may retry on its own, and moving a chat that is about to resume
    would rewrite a live transcript.

  • python orch.py <script> --help prints that script's manual. It printed the driver's own
    docstring for every subcommand, so the menu's promise ("orch.py <script> --help for any of
    them") was false for all of them. The manual is read with ast, never by running the script, so
    the branch stays incapable of acting.


Which Windows download?

file tray icon orchestrator notes
...-windows-x64.zip yes yes The executable plus the misc\ tray toolkit and the orchestrator\ toolbox. Double-click AgentHydra.exe and the daemon starts the tray launcher for you; or run misc\Create-Shortcut.ps1 once for a shortcut that launches through it directly.
...-windows-x64.exe no no One portable file, nothing to unpack. A single file carries no misc\ or orchestrator\ folder, so no tray icon can appear whatever the in-app setting says, and the orchestrator tools report themselves as not installed.

Both self-update, and both are the same daemon. The zip is the complete install; the
single file is the daemon alone, without the two folders that sit beside it — the tray
toolkit and the orchestrator. Each says so plainly rather than looking broken: the .exe
tells you about the tray icon once on first start, and GET /api/orchestrator answers
present:false and names the folder it looked for.