@staix/agent-hub 0.12.39 → 0.12.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +15 -10
- package/docs/agent-notes/adapters.md +4 -1
- package/docs/agent-notes/budget.md +2 -0
- package/docs/agent-notes/bus.md +2 -2
- package/docs/agent-notes/daemon.md +10 -6
- package/docs/agent-notes/models.md +4 -4
- package/docs/agent-notes/tasks.md +3 -0
- package/docs/agent-notes/tests.md +1 -1
- package/docs/memory.md +1 -1
- package/docs/operations.md +152 -28
- package/docs/quickstart.md +19 -5
- package/docs/security.md +19 -7
- package/docs/smoke.md +2 -2
- package/docs/specs/2026-09-19-agent-hub-design.md +158 -70
- package/docs/specs/2026-10-04-switchyard-source-port-design.md +12 -7
- package/docs/verification/2026-10-20-0.12.40.md +63 -0
- package/docs/verified.json +38 -19
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +413 -147
- package/src/adapters/acp.ts +34 -1
- package/src/adapters/claude-channel.ts +35 -4
- package/src/adapters/codex-appserver.ts +12 -3
- package/src/adapters/definitions/builtin-metadata.ts +2 -0
- package/src/adapters/definitions/native-cli-doctor.ts +8 -5
- package/src/adapters/definitions/native-cli-entry-claude.ts +7 -2
- package/src/adapters/definitions/native-cli-entry-codex.ts +13 -14
- package/src/adapters/definitions/native-cli-entry-jcode.ts +13 -3
- package/src/adapters/definitions/native-cli-entry-pi.ts +9 -7
- package/src/adapters/definitions/native-cli-entry-types.ts +2 -0
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +55 -1
- package/src/adapters/jcode.ts +110 -4
- package/src/adapters/local-worker.ts +21 -37
- package/src/adapters/pi.ts +7 -0
- package/src/cli/console-state.ts +1 -0
- package/src/cli/console.ts +11 -1
- package/src/cli/help.ts +13 -8
- package/src/cli/identity-audit.ts +33 -7
- package/src/cli/jcode-mcp.ts +1 -1
- package/src/cli/main.ts +134 -24
- package/src/cli/output.ts +5 -2
- package/src/cli/status-lines.ts +1 -1
- package/src/cli/terminal-recovery.ts +54 -4
- package/src/cli/upgrade-interactive.ts +5 -0
- package/src/cli/upgrade-runtime.ts +13 -1
- package/src/cli/upgrade.ts +67 -1
- package/src/hub/board.ts +4 -2
- package/src/hub/bus.ts +8 -3
- package/src/hub/checks.ts +15 -5
- package/src/hub/conductor.ts +2 -1
- package/src/hub/config-apply.ts +20 -2
- package/src/hub/config-reload.ts +2 -8
- package/src/hub/control-client.ts +26 -3
- package/src/hub/daemon.ts +155 -68
- package/src/hub/delivery-superseded.ts +19 -2
- package/src/hub/events.ts +4 -1
- package/src/hub/hub-tools.ts +4 -4
- package/src/hub/inference.ts +4 -8
- package/src/hub/lifecycle.ts +3 -3
- package/src/hub/main-checkout.ts +36 -0
- package/src/hub/memory-profile-check.ts +6 -2
- package/src/hub/memory-profile.ts +18 -1
- package/src/hub/peers.ts +2 -0
- package/src/hub/ports.ts +1 -0
- package/src/hub/routing.ts +24 -14
- package/src/hub/settings.ts +1 -1
- package/src/hub/task-repository.ts +136 -0
- package/src/hub/tasks.ts +101 -21
- package/src/hub/terminal-launch.ts +130 -0
- package/src/hub/usage.ts +44 -18
- package/src/models/route/config.ts +1 -1
- package/src/omniroute/client.ts +7 -19
- package/templates/AGENTS.block.md +1 -0
- package/templates/routing.toml +10 -44
- package/src/switchyard/config.ts +0 -45
- package/src/switchyard/sidecar.ts +0 -136
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the previous repository, archived on 2026-09-30 when this repository's history was rewritten; the one exception is the open smoke-check issue, formerly #12, which moved here as #1. Numbers in newer entries refer to this repository.
|
|
4
4
|
|
|
5
|
+
## 0.12.40
|
|
6
|
+
|
|
7
|
+
A bug-fix release. Held task offers settle on their own (#434), terminal recovery binds the live native owner (#482), a busy jcode takes an important message into its running turn (#572), and the check runner, hop-capped sends and planner pulls say what happened instead of hanging or claiming success (#558, #550, #531).
|
|
8
|
+
|
|
9
|
+
- Held task offers settle automatically when their recipient already owns and accepted the task, so a peer restart no longer blocks its queue on an obsolete offer; mixed messages, unaccepted offers and requested-stop holds remain held (#434).
|
|
10
|
+
- `ahub jcode --print-command` names a `--model`, `--provider-profile` or `--session-id` that differs from the attached saved session, as the launch does, instead of saying the session is not saved and ready (#574).
|
|
11
|
+
- A completion check killed at its timeout or on hub shutdown now settles within 1 s of the kill even if the child never reports its exit or close, instead of hanging its task (seen as a 20 s test timeout on hosted macOS) (#558).
|
|
12
|
+
- A Claude channel session asks Claude Code to list the hub tools again when a hub reconnect changes them, names or schemas (`tools/list_changed`; an unavailable status probe is retried, then announced anyway), so a session that listed them while the hub was down no longer keeps the short list (#490).
|
|
13
|
+
- `ahub memory profile-check` is split-aware: on a split memory home each running hub is compared with the shard profile of its own project instead of the legacy selector's, and an inactive split reports the `amem migrate --shards --resume` remedy (#536).
|
|
14
|
+
- `ahub jcode` opens its TUI again: on 0.12.39 it stopped after the first hub turn with `jcode: no managed terminal recovery adapter`, because the launcher tried to record a managed terminal that Jcode does not have (#564).
|
|
15
|
+
- Terminal recovery records only a proven native owner, preserves the live row after an unsuccessful second launch, and offers reattach guidance for connected orphaned panes; published unchanged owners skip process probes on ordinary timer ticks. A stale or closed Codex link cannot adopt a thread (#482).
|
|
16
|
+
- Retire the Switchyard sidecar. Model routes run in process, old tables load as retired settings with direct fallback, and doctor checks every configured routing model alias (#553).
|
|
17
|
+
- Refuse Claude channel plugins from an obsolete recorded launch, stop their reconnect loop, and check live socket ownership before a standing-by session reclaims the peer (#551).
|
|
18
|
+
- A busy jcode now takes an `important` hub message into its running turn through its own server socket, instead of the message waiting for the turn to end, and its reply also reaches the sender of what was steered in. The hub never cancels a turn to deliver. For headless Kimi and Qwen, which cannot be steered, `ahub status` and the console show how long an `important` message has waited on a busy turn once it passes `budget.stale_min` (#572).
|
|
19
|
+
- Finishing a task now says `the main checkout is on <branch>; move the work to a worktree under .worktrees/` in the owner's result, the reviewer's request and `ahub status` while the project root checkout is on a branch other than its default (`origin/HEAD`, else `main`). It is advice only (#572).
|
|
20
|
+
- `ahub init` adds one line to the hub instruction block: after pushing a PR, call `hub_task_done` with the PR, head and CI state and end the turn, without waiting for CI (#572).
|
|
21
|
+
- `ahub doctor` lists the extra MCP servers the hub-owned jcode would load from its config files, names only. jcode has no switch to choose them (#572).
|
|
22
|
+
- `hub_task_decline` takes `capacity: true` for "no room right now": the owner is skipped for that move only, and a later reroute or planner pull may offer the task to it again; a plain decline stays a lasting refusal. A planner pull that finds no owner names the cause (a lasting exclusion, WIP full with wip/limit, a held queue or needs_review), and `hub_backlog_plan` refuses a reservation for an excluded peer (#531).
|
|
23
|
+
- Pi and the local worker report a reply dropped at the hop cap as `not delivered: reply chain reached the hop cap (3); send a new message without reply_to` instead of `sent` (#550).
|
|
24
|
+
- Task overlap findings compare only tasks in the same repository. Each task gets one repository key: its `refs.repo`, else the git top-level of its absolute paths outside the project, else the project. `hub_task_propose` and `hub_task_accept` take `refs.repo`, and single-repository boards behave as before (#476).
|
|
25
|
+
- Claude's turn completion is found again when more than 4 MiB of transcript follows the final answer: the reader scans back in 4 MiB chunks up to 64 MiB, so a valid Stop is no longer missed on a long session (#528).
|
|
26
|
+
- A saved console pet palette no longer blocks reloading `.agenthub/config.local.json`: the config schema and live reload accept the documented `pet` block, and removing it no longer deadlocks the reload (#483).
|
|
27
|
+
- A conductor or dashboard start of an attached jcode answers `already` and opens no second terminal; only the console reopens its TUI. A jcode option refusal names the flag (`--model`, `--provider-profile` or `--session-id`) (#559).
|
|
28
|
+
- `ahub doctor` names a malformed `routing.toml` or `routing.local.toml` and its parse error, without the file contents, instead of blaming the gateway's model inventory (#563).
|
|
29
|
+
- Delegated CLI commands record their audit attribution only when the running daemon advertises `cli_audit_delegated`. An older daemon that would drop the record gets a refusal with the upgrade remedy, and an upgrade or recovery from an older source keeps the attribution in its operation receipt (#410).
|
|
30
|
+
|
|
5
31
|
## 0.12.39
|
|
6
32
|
|
|
7
33
|
`ahub jcode` now opens jcode's own TUI on the hub-driven session (#464), and an ACP peer that is running a long tool call is no longer stopped as silent (#542).
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Codex, Kimi Code, Pi and a hub-owned local worker (Qwen Code, Jcode and Antigrav
|
|
|
5
5
|
optional) share a message bus and a task board per project and one memory on the
|
|
6
6
|
machine, each attached through its own native interface.
|
|
7
7
|
|
|
8
|
-
Status: 0.12.
|
|
8
|
+
Status: 0.12.40, control protocol 16. Bun + TypeScript, one loopback daemon per
|
|
9
9
|
project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
|
|
10
10
|
|
|
11
11
|
## What it does today
|
|
@@ -19,7 +19,7 @@ project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
|
|
|
19
19
|
implementation and review, and a designated planner can be changed live.
|
|
20
20
|
Cause-deduplicated stalls and unread pull-only offers need attention; cancellation
|
|
21
21
|
closes work without approving it or releasing its dependents. Owners of
|
|
22
|
-
overlapping work are told what changed.
|
|
22
|
+
overlapping work in the same repository are told what changed.
|
|
23
23
|
- **Shared memory.** Decisions, contracts, fails and findings go to the hub's own
|
|
24
24
|
store through the exact-pinned first-party `@staix/agent-mem` library (short-
|
|
25
25
|
and long-term tiers, a typed graph, git sync between machines),
|
|
@@ -29,8 +29,8 @@ project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
|
|
|
29
29
|
`hub_memory_search`, `hub_memory_show` and `hub_memory_graph` tools. Agents
|
|
30
30
|
can propose corrections; candidates stay outside evidence until a person acts.
|
|
31
31
|
- **Local and remote models.** Pi and the local worker run on DGX or local MLX
|
|
32
|
-
through an authenticated relay;
|
|
33
|
-
|
|
32
|
+
through an authenticated relay; in-process hub routes select models by task
|
|
33
|
+
through a self-hosted OmniRoute gateway. Tasks flagged as PII stay on the on-prem
|
|
34
34
|
worker.
|
|
35
35
|
- **Guard rails.** The local worker and Pi run in a sandbox, and by default their
|
|
36
36
|
writes and shell commands wait for the person's approval; a permission mode
|
|
@@ -83,19 +83,24 @@ writes the running hub's own plugin bundle (#502). Since 0.12.37 the hub accepts
|
|
|
83
83
|
0.98.0 or later, below 1.0.0, and no longer 0.94.0 (#529).
|
|
84
84
|
Since 0.12.39 `ahub jcode` opens Jcode's native TUI on the hub-owned ACP session by
|
|
85
85
|
default (#464); `--mode headless` or `ahub settings set peers.jcode.start_mode headless`
|
|
86
|
-
keeps headless startup.
|
|
86
|
+
keeps headless startup. On 0.12.39 the TUI launcher stopped after the first hub turn;
|
|
87
|
+
0.12.40 opens it again (#564).
|
|
87
88
|
Cancellation needs a daemon advertising `task_cancel`; controlled downgrades
|
|
88
|
-
refuse while cancelled rows exist.
|
|
89
|
-
|
|
89
|
+
refuse while cancelled rows exist. Live-test delegated CLI commands need a daemon
|
|
90
|
+
advertising `cli_audit_delegated` and refuse with an upgrade remedy otherwise
|
|
91
|
+
(#410). For local/Pi implementation, opt in to an available
|
|
92
|
+
`checks.runner = "container"`; issue-backed owners also need authenticated
|
|
90
93
|
spec access. PII work and designated reviewers retain their separate exemptions.
|
|
91
|
-
See [release verification](docs/verification/2026-10-20-0.12.
|
|
94
|
+
See [release verification](docs/verification/2026-10-20-0.12.40.md) for the live legs
|
|
92
95
|
still open, including #418 AC8, #438 AC7, the native console settings leg (#439 AC5),
|
|
93
96
|
real upgrades with reattach (#463) and with a managed terminal and cancelled tasks
|
|
94
97
|
(#478), a live headless Claude review (#223), a Claude session over 32 MiB (#517), a
|
|
95
98
|
live fresh Codex review (#519), a live split of a real memory home (#536), the live
|
|
96
99
|
agy TUI/account leg (#485, hub task #251), Kimi's interactive-TUI coexistence and
|
|
97
100
|
usage checks (#533), Jcode's TUI with a personal account, a real provider and hub-tool
|
|
98
|
-
calls (#464)
|
|
101
|
+
calls (#464), a live Jcode tool call held past the inactivity watchdog (#542), a live
|
|
102
|
+
steer of a busy Jcode turn (#572) and a real upgrade that exercises the terminal owner
|
|
103
|
+
record (#482).
|
|
99
104
|
|
|
100
105
|
Before migrating a memory home with `amem`, upgrade every hub using that home to
|
|
101
106
|
0.12.30 or later, check `ahub memory profile-check`, then quiesce and close all memory
|
|
@@ -105,7 +110,7 @@ every hub using that home to run 0.12.37 or later, which opens memory through sh
|
|
|
105
110
|
(#534). An older hub (0.12.36 or earlier, agent-mem 0.3.1) does not detect the
|
|
106
111
|
split: it keeps reading and writing the old layout, and its writes reach the shards
|
|
107
112
|
only through `amem migrate --shards --resume`. Upgrade or stop every such hub before
|
|
108
|
-
splitting. `ahub memory profile-check` is
|
|
113
|
+
splitting. `ahub memory profile-check` is split-aware (#536). See [memory profiles and rollout](docs/memory.md#selected-memory-profile).
|
|
109
114
|
|
|
110
115
|
## Documentation
|
|
111
116
|
|
|
@@ -57,7 +57,9 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
57
57
|
|
|
58
58
|
- TUI launchers (Kimi, Jcode, agy) write the plugin bundle the hub reports in `status` into their MCP entry, and the CLI's own copy only when the hub reports none (#502). Only the Jcode start path checks the entry against the daemon's own `SERVER_JS` (`jcodeMcpReady`); Kimi TUI admission does not compare it.
|
|
59
59
|
- Jcode (#529): accept the native version only through `jcodeVersionSupported` (from `JCODE_MIN_VERSION` up to, not including, `JCODE_BREAKING_VERSION`), in both the version probe and ACP `expectedAgent`; never pin the minor, and lower the breaking version when a release breaks the contract. Every owned jcode process (probe, serve, ACP and TUI) gets `JCODE_OWNED_ENV`, and the private socket name ends in `.sock`.
|
|
60
|
-
- Jcode TUI (#464): use only the current owned launch's top-level `--socket`/`--resume` tuple after native saved-session proof. Prove the save only through `savedSession` (no-follow, single-link regular file, size-bounded, unchanged across the read, its `id` and `messages` matching) and refuse a requested model, provider profile or session that differs from the running launch. Hand the tuple only to a person's launcher through `native_tui_launch` (admission: `daemon.md`); previews (`--dry-run`, or `--print-command` from anyone but a person) start nothing and never resolve it. Never synthesize a save or prompt to induce one. Ctrl-C during the save wait and frontend close retain ACP; preserve session/load identity refusal and headless owned recovery metadata.
|
|
60
|
+
- Jcode TUI (#464): use only the current owned launch's top-level `--socket`/`--resume` tuple after native saved-session proof. Prove the save only through `savedSession` (no-follow, single-link regular file, size-bounded, unchanged across the read, its `id` and `messages` matching) and refuse a requested model, provider profile or session that differs from the running launch with `JcodeOptionMismatch`, which names the flag, never its value (#559); both the launcher and `--print-command` report it through `refuseNamedOption` (#574). Hand the tuple only to a person's launcher through `native_tui_launch` (admission: `daemon.md`); previews (`--dry-run`, or `--print-command` from anyone but a person) start nothing and never resolve it. Never synthesize a save or prompt to induce one. Ctrl-C during the save wait and frontend close retain ACP; preserve session/load identity refusal and headless owned recovery metadata, and never record the TUI as a managed terminal (`recordTerminalLaunch`): Jcode has no terminal recovery adapter (#564).
|
|
61
|
+
- Jcode steer (#572): reach a busy owned jcode only through `nativeSteer` on its private server socket, never by cancelling its turn: `ping` must list `soft_interrupt_ids`, `subscribe` uses `target_session_id` (the ACP session id) and `continue_on_disconnect`, `state` must report `is_processing` (a soft interrupt sent while idle starts a turn by itself), then `soft_interrupt {content, urgent: false, client_id}`. `canSend` rechecks the captured turn, busy state, owned server process, socket identity and session immediately before the write. Every refusal before the send is `failed_safe` with nothing sent; a sent message that is not acknowledged, or whose turn cannot be confirmed, is `needs_review`, never `failed_safe` or `completed`. Read `currentTurn()` before sending and adopt only through `AcpPeer.adoptIntoTurn(turn, ...)`, which refuses a different or ended turn; adopted senders join the turn answer's audience (`replyParent` and `replyAudience` over the turn envelopes, so the hop cap holds), and each steer receipt is `accepted` at once and settles with the turn (`completed` on a normal end, `needs_review` on a cancel, failure, stop or watchdog). Headless Kimi and Qwen have no steer: status reports their waiting important messages only through `importantWaitingStatus` (`src/hub/daemon.ts`) past `budget.stale_min`. A turn that ends and restarts between jcode's `state` reply and the write still receives the message, held `needs_review` and adopted into nobody: keep that ceiling until upstream offers an atomic per-turn steer, and never add a cancel-to-deliver path.
|
|
62
|
+
- Jcode MCP set (#572): `jcode acp` and `jcode serve` start every server named in `<JCODE_HOME>/mcp.json` and the project's `.jcode/mcp.json`, `.mcp.json` and `.claude/mcp.json`, and no flag or variable selects them (`--mcp-tools`, `JCODE_MCP_TOOLS`, `JCODE_DISABLED_TOOLS` and `JCODE_DISABLE_CLAUDE_MCP` do not). Do not paper over it: `ahub doctor` lists the extra names through `jcodeExtraMcpServers` (`src/adapters/definitions/native-cli-jcode-mcp.ts`), names only, those four paths only, no home search, no link followed. Below the project root or home, `.jcode` and `.claude` must be real directories and the file a regular file (per-component `lstat`, an `O_NOFOLLOW` open and an `fstat` identity recheck); a system alias at or above the configured root stays allowed. Revisit if upstream adds a server selector.
|
|
61
63
|
- For fresh Kimi reviewers and task-bound MCP (#533), retain the common per-child task/head/review-revision/generation verdict fences, privacy screening and cleanup-before-settlement. Expose only a host in-process two-tool MCP capability for `hub_review` and `hub_task_show`; never give the native child the full hub control token. Supply source evidence only through the screened review brief/read result and host facts; exclude native Read/file/shell tools. Admit covered Kimi 2.2.0 only when current runtime preparation passes; otherwise keep it unavailable with the refusal reason. Keep actual interactive-TUI/usage ceilings separate from completed live/fake receipts; do not reuse historical Seatbelt/dummy-provider probes as revised-contract passes.
|
|
62
64
|
- Start fresh Kimi 2.2.0 without Seatbelt, with private `KIMI_CODE_HOME`, managed-provider-only configuration, clean temporary cwd, explicit empty skills directory and exactly `mcp__agent-hub__hub_review` plus `mcp__agent-hub__hub_task_show` in its agent tools. Use directory symlinks for the person's native `credentials/` and `oauth/`, and link `device_id` to the native identity; do not copy token files, select static-API-key mode or set `KIMI_DISABLE_OAUTH_LOCK`. Let native OAuth share refresh/atomic rename/`proper-lockfile` coordination and the existing Kimi account budget. Revoke verdict authority before shutdown; allow bounded TERM grace while the OAuth lock is held, then use verified owned-group teardown. Report a forced-kill/re-login requirement without attempting host auth repair.
|
|
63
65
|
- Keep the default reviewer kind order Claude, Codex, Kimi (selection rules: `tasks.md`).
|
|
@@ -71,3 +73,4 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
71
73
|
- Antigravity P1 (`src/adapters/agy/`): keep it optional, cloud, off roster by default, implementer/verifier only and pull-only. Until per-mode native prompt behavior is verified, reject hub permission/unattended mappings, headless starts and resume selectors; do not infer a quota source. The launcher refuses extra instances, any `agy --version` other than the pinned 1.4.2, and a hub without the `native_hook_launch` feature.
|
|
72
74
|
- Only the person's `ahub agy` launcher manages `.agents/plugins/agent-hub/{plugin.json,mcp_config.json,hooks.json}`. Refuse linked/foreign entries before any write; cleanup only the matching launch. Never write project-level `.agents/hooks.json` or personal Gemini settings. The plugin MCP server is named `agent-hub_agent-hub` by the pinned native client.
|
|
73
75
|
- Managed native facts use the definition's typed `NativeFactsPort` and the daemon's ephemeral `native_hook_launch` authority. Bind every observation to the daemon, launch, native session, project and current socket generation. Stop requires `fullyIdle`, normal termination and a fresh validated completion row; a missing readback keeps the peer busy. Native fact acknowledgement accepts only SDK ephemeral-message rows in the exact session transcript path.
|
|
76
|
+
- `terminalOwner(launchId)` (`src/hub/peers.ts`, #482) answers only for the adapter's current native TUI owner bound to that launch (Codex: the claimed link with that `launchId`, both sockets open; Pi: a started, verified TUI owner on its requested session); a start acknowledgement is never ownership. A Codex link that is not the claimed one or has a closed socket never adopts a thread.
|
|
@@ -21,3 +21,5 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
|
|
|
21
21
|
|
|
22
22
|
- Key quota readings, pause records, overrides, checkpoints and handoffs by the canonical account (`hooks.account`, idempotent); telemetry keeps the reporter id and native context occupancy stays per instance. `instanceBudgetHooks` fans pause, resume, checkpoint and handoff out to every member, and `canHandOff` excludes the paused account's siblings. Startup normalization merges old alias pause rows without dropping a row or marking an unfinished handoff done. A manual pause stays per member. Reviewer variants keep their base kind's `budget.account`.
|
|
23
23
|
- Release-cut quota admission (`releaseQuotaSnapshot` in `src/hub/daemon.ts`, #481 S11): an account has a quota source only when one of its members' definitions has `budget.reader`, or `budget.resetSoftTokensOnResume` with `kimi_tokens_5h > 0`. Without a source a registered account is reported `no quota source` and never defers; with one, an unknown, stale or below-gate reading defers. Manual and budget-record pauses defer for every attached peer.
|
|
24
|
+
|
|
25
|
+
- Keep `readClaudeTranscriptUsage` at its 32 MiB accounting bound. For native turn completion, follow the admission rule in `daemon.md`.
|
package/docs/agent-notes/bus.md
CHANGED
|
@@ -10,7 +10,7 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
10
10
|
- An `important` envelope being steered is not in the queue while the steer is in flight; queue it first and an idle transition delivers it twice.
|
|
11
11
|
- `WsPeer.steer` exists only for a push channel session: set at attach, cleared on detach, roster detach and a pull-only attach, and it refuses anything but important work in the current busy turn (#493).
|
|
12
12
|
- `replyParent()` decides what a reply answers (highest hop, never the `hub` preface). Use it for deliveries and steers alike, or the hop cap can be reset.
|
|
13
|
-
- Report a send's outcome from `Bus.publishResult`, never from `publish()`'s bare target list: a hop-capped send answers `ok: true, dropped: "hop"` with the hop-cap notice, and the channel tool and `ahub say` print that notice, never "no peers attached" (#546).
|
|
13
|
+
- Report a send's outcome from `Bus.publishResult`, never from `publish()`'s bare target list: a hop-capped send answers `ok: true, dropped: "hop"` with the hop-cap notice, and the channel tool and `ahub say` print that notice, never "no peers attached" (#546). A native peer's `onMessage` returns that `PublishResult` on a hop drop, its send tool (Pi, the local worker) reports it as not delivered, and no off-roster retention note follows a hop drop (#550).
|
|
14
14
|
- What a peer is handed (`out`) and what it stands for (`originals`) differ once a delivery is condensed: the bus keeps both, registers `out` so `reply_to` resolves, puts the originals back on a `failed_safe` receipt and holds any other failure (a thrown `deliver`, a later `onFailed`, a `needs_review` receipt) as `needs_review` with its originals, for `ahub queue resolve`, and counts no attempt when the peer merely got busy while the delivery was prepared. A delivery with an `important` envelope is never condensed.
|
|
15
15
|
- A condensed digest is sent by `digest`, not `hub`: `replyParent()` skips only the hub's `presence` item, and a reply to a digest must keep the highest hop of what it replaced. On a `failed_safe` delivery or a `retry` resolution the bus puts back the originals, never the digest.
|
|
16
16
|
- Limits admit what is sent: the envelope `newEnvelope` builds (a reply inherits its parent's sender, `digest` resolves to the originals, `capPriority` applies), never the raw `to` and priority. Admit first and build later, and implicit replies all count as broadcasts.
|
|
@@ -21,7 +21,7 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
21
21
|
- The `onImportantQueued` nudge for a pull-only peer (#398) is advisory: it never dequeues, steers, journals or settles, and a throw from it never stops an enqueue.
|
|
22
22
|
- Merge recall and restored prefaces through `mergePrefaces`/`uniqueMemoryLines`; preserve distinct context and new notes, deduplicate memory identities, and keep colliding short prefixes distinct. Use `recallDisplayText`/`recallMemoryLines` before display caps: suppress only matching automatic approval outcomes, retain nonempty rejection details, preserve id headers and complete-word excerpts, and keep token trimming at line boundaries. Keep these changes display-only.
|
|
23
23
|
- Observe memory rendering from final `out`, not `originals`, and suppress telemetry when either contains private envelopes. A pull emits rendered/delivered observations only after its journal transaction commits; observer exceptions must never change delivery settlement.
|
|
24
|
-
- Classify automatic held-row completion only through `supersededDeliveryReason`: every original must be a hub presence or a provably moot hub task/review notice. Unknown/live/mixed rows stay held; keep `deliveryTaskContext` display-only. Tag only current-owner notices as owner audience; leave former-owner withdrawals untagged until the task closes. For cancelled tasks, settle earlier workflow notices but retain the stop notice whose `refs.taskCancelled` matches the cancellation revision. Preserve the requested-stop exclusion in `daemon.md`. Record the hub actor through `resolveDelivery`, restoring presence originals through `restorePreface` only for hub completion (person completion stays consumed); run on hold entry, restore, recovery release and board changes, never replay uncertain work. List every hold once in status, console notices and route explanations (#434).
|
|
24
|
+
- Classify automatic held-row completion only through `supersededDeliveryReason`: every original must be a hub presence or a provably moot hub task/review notice. An owner offer (`refs.taskOffer`) is moot only while that recipient owns the task in `in_progress`, `in_review` or `changes_requested` and its own `accepted` entry follows the latest ownership event within the 4096-entry bound; a person's acceptance, an unmarked reminder or missing proof keeps the hold (#434). Unknown/live/mixed rows stay held; keep `deliveryTaskContext` display-only. Tag only current-owner notices as owner audience; leave former-owner withdrawals untagged until the task closes. For cancelled tasks, settle earlier workflow notices but retain the stop notice whose `refs.taskCancelled` matches the cancellation revision. Preserve the requested-stop exclusion in `daemon.md`. Record the hub actor through `resolveDelivery`, restoring presence originals through `restorePreface` only for hub completion (person completion stays consumed); run on hold entry, restore, recovery release and board changes, never replay uncertain work. List every hold once in status, console notices and route explanations (#434).
|
|
25
25
|
- `resolveDelivery` mutates queues in place; a refused or failed resolution puts back only that peer's queue and preface and keeps every pause. Never reload a snapshot on a live bus. Keep the selected delivery id and journal revision fixed in console row actions; retry/discard/completed never change a task role or cancel a native turn.
|
|
26
26
|
- Recovery-deferred hub notices (`deferredHub`, #478) are accepted work kept outside the queues and the carried-queue integrity projection: never put them in `queues` or the integrity view while held. Every read of pending work includes them (`pendingEnvelopes`, `withdraw`, `revokeSupervision`, `queuedBacklogIds`, `get`, reset). Past `MAX_DEFERRED_HUB` refuse the publish; never drop a kept notice.
|
|
27
27
|
- Release moves each deferred notice through `publishNow` and checkpoints it before delivery, events or nudges run. A failed checkpoint restores only publication state (the affected queue arrays in place, deferred, seen and withdrawn ids, pending outcomes) and keeps the hold. Never reload a snapshot or touch prefaces, pauses or the recovery mode from it. A successful release clears the deferral mode (`deferHub`) after `flushDeferredHub()` and before dropping `recoveryHeld`, so a later bare hold or `fenceRecovery` queues hub notices; a failed flush keeps both the hold and the deferral mode (#541).
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
- For explicitly user-authorized live verification, use the per-invocation CLI delegation in `docs/security.md` §Agent CLI identity and conductor authority; keep caller markers and record native evidence separately from fixtures.
|
|
4
4
|
- Keep `export const LIVE_TEST_SUPPORTED = true;` in `src/cli/identity.ts`: delegated recovery staging reads this declaration before any source effect.
|
|
5
|
+
- A delegated non-recovery command runs only after `recordDelegatedCliAudit` committed `delegated` against a hub whose `features` list `cli_audit_delegated`, that instance's console `cli_audit_ack` confirmed retention, and `ControlClient.pinInstance` pinned the invocation to it; a refused feature, failed ack or changed consumer executes nothing (#410). Upgrade, restart and recovery record delegation only in the operation receipt (`delegatedAudits` through `createOperation` or `recordRecoveryDelegation`). The daemon counts an audit retained only after `event()` returned true.
|
|
5
6
|
|
|
6
7
|
- Console attention navigation selects a delivery only by an exact visible row id. A missing id or row prints read-only guidance; never select by peer or fall back to the first Queue row.
|
|
7
8
|
|
|
@@ -12,15 +13,17 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
12
13
|
- Settings (#269): the dashboard's `setting`, `setting_undo`, `setting_preview` and `setting_reload` actions and the console-role `settings_get|set|undo|preview|reload` requests go through `settingsSet`, `settingsUndo`, `settingsPreview` (`settingsUndoPreview` for `undo: true`) and `reloadConfig` (`src/hub/daemon.ts`) and the registry in `src/hub/settings.ts`. A new editable setting is a registry entry, never a new write path; a key that chooses what the hub runs or where secrets or task text go joins `NEVER_EDITABLE` instead. Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml` through `store()`: the loader's own validation first, temp file and rename, never a file git tracks. Authority comes from the dashboard session (`DashboardSession`, `src/hub/ui.ts`) or the console role, never from the request body; `ui_action` from the manager is an ordinary session. Never return a setting's value in an event or notice except through `recordSetting`.
|
|
13
14
|
- Live reload (#439): `reloadConfig` is the only publication path. Settings writes and undo, `ahub settings reload`, the dashboard reload and the debounced watcher of the four `CONFIG_SOURCE_FILES` (`src/hub/config-reload.ts`) all reach it; `prepareConfiguration` validates the complete candidate before anything is published, and recovery or shutdown holds it. A write or undo passes its own key as `scope` and publishes only that path, and `acknowledgeConfiguration` records only the accepted paths, so other edits stay pending. Every changed key, inside the registry or not, is decided by `authorizeConfigLeaf` (`src/hub/config-apply.ts`): a refused key keeps its accepted value and is reported by name while the others apply, `roster.*` applies only through its own scoped roster write, and a change of the settled planner or conductor holders (`roleHolders` over `resolveSettledRoles`) is guarded even when a role list shrinks. Outside a named write, only the console's `settings_reload` (authority `terminal`) adopts guarded file edits, by sending back the `confirmation` token bound to the candidate `fingerprint` and `configRevision`; changed bytes or accepted state refuse it, and a watcher, dashboard or settings session never confirms. An undo that names `key` and `undoDigest` writes only while `pendingUndoIdentity` still matches both. A `restart` key (`configApply`) keeps its running value and is listed in `pendingRestart`; reload notices and events carry key names and reasons, never values. The console Settings panel arms a write only from a preview reply that matches its open edit's token, key and value, and pasted text never confirms.
|
|
14
15
|
- Retained configuration (#544): a key the reload refused, or a file edit outside a scoped write that still differs from the accepted value, is listed in `retainedConfig` (key and sanitized reason only, never a value) in status, `status.json`, the settings view and the console snapshot until a reload accepts or reverts it (`retainedConfigAfterReload`). A config leaf that selects a peer's native launch belongs in that definition's `cli.launchConfigKeys`: the `start` and `peer_start` replies then carry `retainedLaunchConfig` (the running values), the launcher starts with them and prints `retainedLaunchWarning` before its start request.
|
|
15
|
-
-
|
|
16
|
+
- `pet` is console-local presentation (#483): validate it only through `validatePetConfig`, which both `loadConfig` and the reload schema call, and keep it applied live with no authority (`authorizeConfigLeaf`).
|
|
17
|
+
- Start modes (#269): every start the hub makes itself goes through `hubStart` (`src/hub/daemon.ts`): the dashboard's `start_peer`, the console-role `peer_start`, the conductor's `start` hook and `pi.auto_start`. It takes the mode from `startMode(peer)` and nothing else; never add a caller-chosen mode, flag or command to it, and never fall back to headless when a terminal cannot be opened (refuse with the manual command). The words a terminal runs are assembled there from `process.execPath`, the CLI entry, the project root and `startWords(peer)`. The only continuity exception is a validated Pi bookmark on `pi.auto_start`: append its recorded session file (or its session id when no file was recorded), backend and model; never pass both session selectors or accept caller-written replacements. Recovery and `startPeer` callers that pass a recorded mode keep it. A launcher with no `--mode` takes the daemon's accepted start mode from `status.json` `startModes` (`readControl`) before the file's value. A TUI start of an `owned-acp` peer (Jcode) from the console's `peer_start` opens its terminal even while the ACP owner is online: that terminal is a frontend on the owner. The conductor and the dashboard answer `already` for an attached one (#559). The real terminal providers are wired only in `src/hub/lifecycle.ts` (`DaemonOptions.terminal`): a daemon built without them, as in every test, opens nothing; a test injects a recording provider and never the real one.
|
|
16
18
|
- `ahub setup` reads Claude Code's state from `--json` listings and takes one step at a time, re-reading after each; never match paths or names by substring.
|
|
17
19
|
- A copy of the git index keeps the original's mtime (`snapshot()`): git's racy-entry check compares entries with the index file's time in whole seconds, and a fresh copy makes a same-size edit look clean.
|
|
18
20
|
- State files that clients read (`status.json`, `control-token`) are written after the port is bound, and `status.json` via temp file + rename.
|
|
19
21
|
- Open native memory through the shared profile-aware helper with an explicit current selector binding; never fall back to shared v1 after invalid/missing generation metadata. Check that binding at hub call-site entry and before candidate/counter writes, including callbacks retained across awaits; do not intercept store methods with proxies/private hooks or claim atomic in-flight cutover. Quiesce and close consumers before migration/selection changes, then reopen through the supported lifecycle. Keep the sidecar at the independent memory root, not inside the selected generation. Report memory availability separately from bus health; never hot-rebind or migrate as a startup repair (`docs/memory.md` §Selected memory profile).
|
|
20
22
|
- Take the memory project only from `memoryProjectFor` (the alias-resolved `memoryProjectKey`) in the daemon, `ahub memory` and `ahub goal`; never use a raw `memoryProjectKey` as the project of a store, screen or raw command. The daemon resolves it once at start (falling back to the raw key, so the memory home never blocks startup) and passes both `project` and the raw `projectKey`, so the binding latches unavailable once the alias log maps the key elsewhere; a restart binds the new key.
|
|
21
|
-
- Detect a curated split by `lstat` of `shards.json` (`splitMarked`), never `existsSync`. An inactive, malformed, dangling or unreadable marker throws `MEMORY_SPLIT_INCOMPLETE` before any shard read, and callers show that message instead of the generic profile refusal. On a split home open only through `openShardStore` with the canonical memory dir and a project key; never a plain `MemoryStore`, and refuse without a key.
|
|
23
|
+
- Detect a curated split by `lstat` of `shards.json` (`splitMarked`), never `existsSync`. An inactive, malformed, dangling or unreadable marker throws `MEMORY_SPLIT_INCOMPLETE` before any shard read, and callers show that message instead of the generic profile refusal. On a split home open only through `openShardStore` with the canonical memory dir and a project key; never a plain `MemoryStore`, and refuse without a key. Profile inspection and `profile-check` on a split home read the shard seals (`shardProfileForProject` for each hub's own project), never the legacy selector (#536).
|
|
22
24
|
- The channel's reconnect loop stops on closes a retry cannot fix (`TERMINAL_CLOSES` in `claude-channel.ts`); a new daemon close code that means "do not come back" belongs there, or two clients fight over it forever.
|
|
23
|
-
-
|
|
25
|
+
- The channel advertises `tools.listChanged`; build its tool list only through `toolListFor` and record `listedTools` from each `ListTools` reply, so a reconnect sends `sendToolListChanged` when the whole definitions differ or the lifecycle stays unreadable (#490).
|
|
26
|
+
- Close 4000 is the one close that ends on its own, so it is not in `TERMINAL_CLOSES`: the session stands by and reconnects only once `status.json` reports the peer offline and not `claiming`. One exception (#205, `peerHeld` in `claude-channel.ts`): a session with channel pushes also reconnects when the holder is reported `toolsOnly` and not `claiming`, and takes the peer; the daemon refuses the reverse with 4000. Reconnecting blind would evict whoever holds the id now, and the two would trade it forever. `status.json` is advisory: a reconnecting standby sends `standby: true`, and the daemon refuses it with 4000 while `standbyHeld` holds (a claim, or an open socket; only a channel-capable standby may replace a pull-only holder). A hello whose `launchId` differs from this instance's current `claude-launch` record closes with 4430, which is in `TERMINAL_CLOSES`; no launch id or no record keeps the old admission (#551). `claim()` happens at hello and `attach()` only after the preface, so the peer reads as offline in between: the daemon writes the status file at the claim and the flag covers that window.
|
|
24
27
|
- Adding a native peer requires testing the command emitted by the actual recovery driver, not only its inspection or launch helpers. Never let a generic non-Codex branch treat a new peer as Claude.
|
|
25
28
|
- Cross-version recovery must use the authenticated source protocol for prepare/commit/abort and the target protocol after startup. Prove the transition against a real prior release before claiming compatibility.
|
|
26
29
|
- Preserve `task_cancel` target-feature preflight before source holds, capture or commit. Reuse stored target features only after verifying the same retained root and digest; legacy receipts still capable of commit must obtain feature proof. Do not apply pre-commit compatibility checks to sources already past commit. Every prepare, including the lease renewal before a terminal close and after a resume, sends the operation's stored `targetFeatures` (#478).
|
|
@@ -36,7 +39,8 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
36
39
|
- After a managed terminal is created, persist its binding before waiting up to `RECONNECT_WAIT_MS` for authenticated attachment to the expected target instance, operation and conversation (#414). Resume waits on the saved binding without creating another terminal. Count only attached, non-offline session ids; when the chosen terminal has no current launch record, a confirmed-ended row for another handle or an id-less attachment retains the bounded wait after older-launcher checks. A late current record must match the saved terminal; unknown or live replacement evidence still blocks. Timeout keeps the receipt blocked and records only terminal/hub metadata; a last native connect attempt that was never recorded is unknown, never inferred from terminal readiness or offline state. Final identity and preservation verification stays strict.
|
|
37
40
|
- An operation receipt stays `schema: 1`. Only `abandoned()` writes 2, on an operation with a recorded `disposition` whose coordinator predates #215: in the same write as the disposition, or, for a disposition 0.12.20 recorded at 1, by the next command that holds the runner claim. `validateReceipt` accepts 2 only with a disposition (#227). Never write 2 on an operation that can still be resumed.
|
|
38
41
|
- Four checks read another release's source text for an exact line: `coordinatorCurrent` (`export async function disposeRecovery` in `src/cli/upgrade.ts`), `targetReadsWaivers` (`export function readRecoveryWaivers` in `src/hub/restart.ts`), `coordinatorRestarts` (`export function recordRestart` in `src/cli/upgrade.ts`) and `supportsAction` (`export async function reattachRecovery` in `src/cli/upgrade.ts`). Renaming, moving or un-exporting one makes this release read as an older one: keep those lines until a capability list replaces the checks. The reattach CLI dispatch must use the same `supportsAction` check as its printed command; never redirect a fallback command back to a coordinator without that action. Recovery also reads a target's literal `OPTIONAL_PEERS` (`src/hub/peer-catalog.ts`, through `legacyTargetSupport`) and `START_MODE_PEERS` (`src/hub/start-mode.ts`, through `legacyHeadlessArgv`), declared in `src/adapters/definitions/native-recovery.ts`; keep both literal declarations until a published capability replaces them.
|
|
39
|
-
-
|
|
42
|
+
- Publish terminal ownership only after the current native owner is proven through `terminal_owner` (#482); capture and start acknowledgement are pending evidence only. Preserve unrelated and invalid rows, scope current evidence to project/instance/peer, and recheck the same native generation after asynchronous Orca reads. Keep Claude's hook record pre-spawn. Compatibility and retention ceilings are described in Operations, controlled recovery.
|
|
43
|
+
- `reattachRecovery` (`src/cli/upgrade.ts`, #463) relaunches only the peer a blocked receipt's `blockedReattachment` names, once per its `reattachments` marker, after the lock, the runner claim and a re-read, and only against the same held target instance and version. The terminal proof is `inspectSavedTerminal` (`src/cli/terminal-recovery.ts`): only an `orphaned` pane without positive connected evidence or `missing` (a typed stale handle and a complete, untruncated inventory) counts as gone; `unknown`, `changed`, an orphaned-but-connected pane or a disconnected-only reading never authorizes a terminal. It counts as reattached only on a `done` restore receipt with the exact recorded session id, session file and project root, then leaves the operation blocked for `recovery resume`. It is person-only: `classifyPeerCommand` and `liveTestRefusal` refuse it from an agent shell.
|
|
40
44
|
- The interactive screens (`src/cli/upgrade-interactive.ts`, #272) change an operation only by running an existing command (`recovery resume|abort|dispose|reattach`, `reset`) through `io.run`, and build their menu from `nextChoices`, whose text is `nextActions`: never call `abortRecovery`, `disposeRecovery` or a driver from a screen, and never offer a choice `next` does not have. The one addition is `cancellableWait` (a runner still at `prepare` with no effect): read the receipt again at the key press, stop only a runner `signedRunner` names (a claim without a matching process signature is never signalled), then run `recovery abort`, which decides again. Agents are ended (`endPlannedPeer`) only before an operation holds the lock, and only after the lock, the hub instance, the attached session and the terminal's handle and incarnation were read again: never on the plan as it was drawn. Ctrl+C leaves the screens and the terminal is not read after it; a prompt answers an interrupt with null, never text. What a hub says it waits for (`recovery.waiting`, the daemon's `blockers`) is shown, never decided on.
|
|
41
45
|
- Recovery presentation (#466, `src/cli/upgrade-presentation*.ts`) only renders, and only when `humanRecovery` holds (a TTY and no `--json`); `--json` stays the raw document. Recognise `--json`, `--verbose` and `--color=` only through `recoveryPresentationFlag` (`src/cli/recovery-presentation-options.ts`), which `liveTestRefusal` and `classifyPeerCommand` share, so a presentation flag cannot hide reattach or a repaired dispose from authorization. A screen's Enter default (`preferred`) is read-only (refresh, follow the runner, read the receipt); apply, resume, reattach and destructive choices need their own key, and an item with a `disabledReason` never runs. The receipt's `waits` and `presentationCause` describe the current attempt only: written best-effort (a failed timing save warns, never fails the operation or replaces the wait's own error) and cleared at each resume. Without them show `timing unknown` or an unknown cause; never substitute `updatedAt` or guess a cause from error text. With no typed cause, `renderBlocked` may show only the first recorded error line through `terminalText`, controls stripped, capped at 512 code points and labelled `typed cause unknown`, with the uniform action; never classify it or choose an option from its text (#478). `endInspection` (the completion and stop-and-archive summary) only inspects, and a failed reading is `runtime unknown`.
|
|
42
46
|
- `upgrade()` (`src/cli/main.ts`, #272) never coordinates a target newer than its own `VERSION`: it hands over to that release's coordinator (`bun x --package @staix/agent-hub@<to> ahub upgrade --to <to>` with the same flags), and `AGENTHUB_UPGRADE_HANDOVER=1` keeps the process it started from handing over again. `--to` passes `exactVersion` before it reaches a package spec or the registry. `latestRelease` supplies it only for a terminal run or a dry run: `--yes`, and an apply through a pipe, still name the release.
|
|
@@ -54,7 +58,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
54
58
|
|
|
55
59
|
- A Kimi one-launch TUI admission ends on its owning detach, roster removal, matched launcher cleanup or the unclaimed 30 s deadline. An old socket or launcher never clears a newer admission. Pin --mode tui in the hub-opened terminal argv.
|
|
56
60
|
- `native_hook_launch` (#543) is console-only and needs a definition with `hooks.factsHook`; one launch per peer. Refuse `begin` during recovery or shutdown, off roster or while the peer is attached. `end` needs the matching `launchId` and is the only request besides `status` and `kill` a stopping hub admits; closing the launcher's socket ends its launch. Accept `native_facts` only from the tools role for the current `instanceId` and `launchId`, the latched native session and socket generation, and workspace paths whose `realPath` is the project. Close a `ws-pull` peer's hello without `channel: false` with 4403.
|
|
57
|
-
- `native_tui_launch` (#464) is console-only and resolves only a running owned `JcodePeer` under its own kind id (`tuiLaunch`). Refuse it while stopping, during recovery, off roster, during that peer's roster change or requested stop, or when `jcodeMcpReady` fails against the daemon's `SERVER_JS`; a `binding` must name the current `launchId` and `sessionId`. Failures answer fixed errors with details withheld, and the launch tuple (socket, env) goes only into that console reply, never into `status`, `status.json` or a log. A launcher sends it only when the hub's `features` list `native_tui_launch`.
|
|
61
|
+
- `native_tui_launch` (#464) is console-only and resolves only a running owned `JcodePeer` under its own kind id (`tuiLaunch`). Refuse it while stopping, during recovery, off roster, during that peer's roster change or requested stop, or when `jcodeMcpReady` fails against the daemon's `SERVER_JS`; a `binding` must name the current `launchId` and `sessionId`. Failures answer fixed errors with details withheld (a `JcodeOptionMismatch` names only the flag in `option`, #559), and the launch tuple (socket, env) goes only into that console reply, never into `status`, `status.json` or a log. A launcher sends it only when the hub's `features` list `native_tui_launch`.
|
|
58
62
|
|
|
59
63
|
- When preserving a recovery source, copy only the exact-pinned `@staix/agent-mem` package through `preserveRuntimeDependencies` in `src/cli/recovery-package.ts`; include its identity and bytes in `packageDigest`. Never copy the whole node_modules tree or link a preserved source to a mutable installation. Verify preserved sources with `ownDependencies: true` so a missing or symlinked memory copy cannot resolve an ancestor installation. Verify staged releases with `dependencyRoot` bound to their retained install directory; never resolve beyond it. Keep no-dependency release digests compatible.
|
|
60
64
|
|
|
@@ -71,4 +75,4 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
71
75
|
|
|
72
76
|
- Name every Claude per-peer state file through `claudeStateFile` (`src/hub/claude-state.ts`) with that peer's id, never a hard-coded builtin name; pick a peer's `terminal-recovery.json` row only through `ownTerminalRow`. `sessionPointers` in `src/cli/reset.ts` must match any new per-peer pointer (#450 I3).
|
|
73
77
|
- A declared Claude channel extra is never hub-started: keep it out of `STARTABLE_PEERS`, instance start modes and `RESERVED_IDS`. A launcher admits `--as <id>` only when the running daemon's status `activeInstances` lists it (`requireActiveInstance`), never from the config on disk.
|
|
74
|
-
- Verify a native Claude Stop and its start baseline only through `readClaudeTranscriptCompletion` (
|
|
78
|
+
- Verify a native Claude Stop and its start baseline only through `readClaudeTranscriptCompletion` (4 MiB backwards chunks, at most 64 MiB plus one boundary byte; incomplete or out-of-bound rows are unavailable), never the size-capped `readClaudeTranscriptUsage`; an `unavailable` refusal leaves the peer busy and notifies once per turn start (#517).
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Model relay, inference and gateway
|
|
2
2
|
|
|
3
|
-
Scope: the model relay and its journal, the hub's own inference and its slots, OmniRoute and
|
|
3
|
+
Scope: the model relay and its journal, the hub's own inference and its slots, OmniRoute and in-process model routes. Read before editing `src/models/`, `src/hub/inference.ts` or `src/omniroute/`.
|
|
4
4
|
|
|
5
5
|
- The model relay journals per-request identity (`RelayRequestRecord`): a request's served model comes only from its own gateway header, its own generation SSE event (#137 heartbeat classification), or the locally validated MLX configuration. A backend's mutable last-served label is never a request's evidence, HTTP 200 plus the requested alias identifies nothing, and a request cancelled before identification stays explicitly unidentified (`identified: false`, `outcome: "cancelled"`). Journal records carry no prompts, tools, keys or Access headers.
|
|
6
6
|
- Inference is optional and fail-open: it returns its input or `undefined` on any failure and backs off, so a delivery never waits on a dead model twice. Its output is only ever capped text or a value checked against a closed list.
|
|
7
|
-
- The PII screen (`screenPii`, #198) is the exception: it fails closed. Every failure (timeout, no or off-campus model, an answer outside `clear` / `pii <category>`, text over `SCREEN_MAX_BYTES` UTF-8 bytes, since a token is at least a byte) is `unknown`, which `Tasks` handles as PII. It never goes through the
|
|
8
|
-
- Secrets stay inside `OmniRoute`: never put the key or Access values in a log line, an error message, a return value or
|
|
9
|
-
-
|
|
7
|
+
- The PII screen (`screenPii`, #198) is the exception: it fails closed. Every failure (timeout, no or off-campus model, an answer outside `clear` / `pii <category>`, text over `SCREEN_MAX_BYTES` UTF-8 bytes, since a token is at least a byte) is `unknown`, which `Tasks` handles as PII. It never goes through the inference backoff, asks the gateway only after `onCampus()` and with `onCampusOnly`, takes the on-device generation slot under its own deadline (`acquire(abort.signal, 0)` first, one look and `MlxBusyError` when all are taken: a taken slot sends it to the campus gateway, and only off campus does it wait; on campus a failing or still-loading device hands over to the gateway after 60% of the deadline), sends temperature 0 and `reasoning_effort: "none"` on both paths so a reasoning model cannot spend its 32 tokens thinking, and never puts the text in a log line, an error or an event.
|
|
8
|
+
- Secrets stay inside `OmniRoute`: never put the key or Access values in a log line, an error message, a return value or routing configuration.
|
|
9
|
+
- Keep model selection in process; retain the Apache-2.0 notices in `src/models/route/` and the upstream provenance check. Treat legacy routing tables as retired and never rewrite operator files during load.
|
|
10
10
|
- Shared inference slots must recover after a hub process dies, without evicting a live owner. Cancellation has to reach slot acquisition from the relay caller, not just exist in the helper signature.
|
|
11
11
|
- Stay/switch (#197): `hub/auto` and local stage routes apply `stay_switch` only through `stayOrSwitch` (`src/models/route/stage.ts`), never at a call site; `shadow` must not change a routing decision (the recorded-decision golden test in `test/route-relay.test.ts`). No route summarizes, trims or rewrites the forwarded conversation: a tier that cannot fit is filtered out. A PII route's planner never sees the conversation size (no prefill bound) and its event omits `prefillTokens`.
|
|
12
12
|
- Load and cooldowns (#199) only reorder a `hub/auto` request's candidates (the route's own backend, then its fallback, as on main), in one ordering step in the relay's fetch handler; a fixed alias is never reordered. Never remove a candidate. A candidate goes first only while it is neither cooling nor `failing` (any failure, an error status or a failed stream included, for 30 s or until a success; separate from the cooldown streak). Load and pin moves are opt-in (`efficientWaitMs` absent: none) until #199 AC5 measures them. Their attempt is bounded only up to its response headers (`MOVE_FIRST_BYTE_MS`, after `omni.base()`); a cut there marks `failing` without counting toward a cooldown, and a stream that fails after the headers cannot fall back, so do not claim a move never fails a request MLX would have served. None happens while the request runs under an execution budget (`underBudget`, a read that admits nothing; a throw counts as a budget). The selector only names `prefer`; it never picks a moved backend itself. They never consider PII because the relay serves only Pi, which refuses PII work before any model call (`selectModel` in `daemon.ts`); a relay caller that can carry PII must not get a reorder toward DGX without a positive `onCampus()`. Count a cooldown failure only for a transport or startup failure outside a cooldown that was neither cancelled, refused admission, a busy MLX slot (`MlxBusyError`: load, never failure), nor a deadline the relay cut itself (`journalEntry.cut`: `"budget"` for the execution budget's, `"move"` for a moved attempt's first byte, which only marks `failing`); any HTTP answer, whatever its status, ends the streak and the cooldown (`answered`).
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/settled-roles.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts`, `src/hub/hub-tools.ts`, `src/hub/review-brief.ts`, `src/hub/reviewer-pool.ts`, `src/hub/reviewer-pool-config.ts` or `src/hub/release-admission.ts`.
|
|
4
4
|
|
|
5
|
+
- Scope every task-place comparison through `TaskRepositories` (`src/hub/task-repository.ts`, #476): explicit `refs.repo`, else a common outside-absolute-ref repository or parent, else this project. Preserve relative display spelling within that key and gate overlap, contradiction, replay and held evidence by equal keys. Warm/cache repository identity at task mutation and startup; never repeat Git reads for unchanged overlap inputs. Set `refs.repo` for work in another repository; repo-only accept metadata never grants execution or file access.
|
|
5
6
|
- Tool callers are models: MCP `inputSchema` is not enforced on the way in. Normalize at the boundary (`cleanRefs`) before anything reaches the board, and never throw after a board write: a publish or notify that follows a `review()` or `done()` board write goes through the one `releaseNotice` guard, which turns a failed publish into a console notice, drops a failed notify and never retries (#231, #243). An assignment's delivery once its board write is saved (#276, #297): the move is fact, the failed publish is not retried in place (the journal's own recovery covers a storage error, and a retry may duplicate a publish that actually left), and a person looks for a queued or needs_review delivery with `ahub queue list --peer <owner>` (then `ahub queue show <id>`, or the accepted entry in `ahub task show <id>` if the journal is down) before re-sending with `ahub task assign <id> <owner>`. The distinction is made once, in `assignOwner`: a failure after the write prints the one console notice from `undeliveredNotice` right there, once, and throws `AssignmentUndeliveredError` carrying the saved task; callers (`review()`'s repeated rejection, `escalate` and its callers, the ready offer, both relays, the idle sweep, rescreen) decide control flow by the type and print nothing more, and `propose`, `assignTo` and `decline` additionally let the error reach the caller in its turn, which is a person or peer that can act. Two exceptions get no notice, by construction: a row superseded meanwhile, meaning a later history entry that moves or settles the task (another assignment, an unassign, the owner's accept, done, approval, close), while entries that do neither (the hub's quiet bookkeeping: answers, reviewer changes, idle sweeps, check results) leave the assignment undelivered and the notice stands, so the original error goes to the caller as before only on supersession; and a claim's own accepted write and "claimed by" line stay outside the delivery rule (a claim is its own hand-over; its overlap and routing announcements to earlier owners are deliveries and share the wrapper, so a failure there does read as one). A failure before the write stays a plain `Error` (#254), a failed write leaves the ready offer unconsumed (#231), and the notices before the write (waits, no-owner, the reserved-owner line) go through `releaseNotice` too, so those pre-write notices cannot fail their caller.
|
|
6
7
|
- Post-await assignment publication is fenced by current role authority: supersession returns the current task without a saved-delivery notice; reviewer-only changes refresh current metadata and do not suppress a valid owner delivery. A replacement notice goes only to a role that actually received the request, not to an unread-only queue.
|
|
7
8
|
- Console task row assignment/unassignment carries the exact role, task id and `flowRevision` (history length), checks it before effects and passes it to `Board.update`; a changed revision requires reload. Person owner removal clears owner/reservation and suppresses automatic offers until a later explicit person assignment/reservation; refuse owner removal under review or after approval, and preserve the required PII person reviewer.
|
|
8
9
|
- Role withdrawal matches the task id, role kind and owner-request header. Owner acceptance evidence must be that peer's accepted entry after the latest ownership event, never another owner's state or an earlier assignment span. Withdraw unread requests without inventing delivery evidence. Find them with `bus.pendingEnvelopes(peer)`, which includes recovery-deferred notices, never the snapshot's queues (#478). Preserve the original former-owner stop notice after a superseding escalation, while suppressing stale success/decision tails.
|
|
10
|
+
- Capacity declines (#531): `hub_task_decline` takes an optional `capacity: true`, recorded as `capacity: true` on the `declined` history entry. `excluded()` skips such an entry, and `Tasks.decline` passes the decliner as `exclude` to that move's own reroute only, so a later reroute or planner pull may offer the task to it again from its reservation. Never infer a capacity decline from the note text, and never treat a row without the field as one: older rows are refusals. `plannerRefusal` (peer ids and counts only, never task text) names why `pullPlanned` found no owner, and `planBacklog` refuses a reservation for a peer `excluded()` returns. `assign()` stays pure, so `route explain` reads the same `excluded()` and `plannerFull` inputs as assignment.
|
|
9
11
|
- `assign()` never defaults to the task's current owner, or a decline can only come back to the decliner. A reservation (#207) is the same risk: the reserved owner goes first only through `blocked()`, and a decline or an escalation for reason `rejections` or `manual` records the owner it left in the history entry's `from`, which `excluded()` reads, so no later reroute hands the task back through its reservation.
|
|
10
12
|
- The state machine allows `in_progress -> approved` only for classes without a reviewer; `review()` checks `in_review` itself.
|
|
11
13
|
- Treat `cancelled` as closed through `isClosedTaskState`, but keep dependency release restricted to approval. Cancellation goes through `Tasks.cancel`, preserves the row and roles, clears its reservation/cohort, sends revision-bound stop notices and writes no outcome memory; never map it to approval or cancel a native turn or worktree. Planner cancellation stays limited to its own historically unaccepted proposed/backlog work; accepted work remains person-only.
|
|
@@ -52,3 +54,4 @@ Scope: the task flow, assignment, the state machine and the hub's MCP tools. Rea
|
|
|
52
54
|
- `Tasks.retryReviewer` is host-only (the daemon's reviewer `onUnavailable`), never a tool or CLI path. It acts only while the failed peer is still the reviewer of an `in_review`, non-PII, not manually named task, with the same review head, the request's `reviewRequestRevision` and a current child generation; `valid()` rechecks all of it after every await and right before the write, which also passes the flow revision. Each failed peer is tried once per head and round (`reviewerFailure` history). `reviewRequestRevision` counts only role events (`done`, `reviewer changed`, `reviewer unavailable`, ownership), so bookkeeping never fences a request; keep `refs.reviewRevision` and `refs.commit` on the review envelope.
|
|
53
55
|
- Release trains (#481 S11): change trains only through `Tasks.newRelease`, `configureRelease`, `assignRelease` and `cutRelease` (planner or person), and refuse every `hub_release_*` in a PII turn. `assignRelease` writes only `release` and refuses a frozen, shipped or dropped release on either side; private or pinned scope is person-only. One active or frozen train at a time. A cut freezes only through `Board.freezeRelease` after a fresh `releaseWindow` (host lease from `readReleaseHostAdmission`, quota, `current()`, at most 5 s old); unknown evidence defers, and the window is closed in `finally`. A cut never publishes, tags or upgrades, and upgrade and recovery never consult `releaseWindow`. Keep two of `RELEASE_TRAIN_HISTORY_LIMIT` slots for cut and ship; a configuration no-op appends nothing. An S9 plan receipt survives only contiguous planning revisions that changed exactly `["release"]`.
|
|
54
56
|
- `Board.update`'s untouched-return exception is the only way owned or accepted work returns to backlog: keep every condition (exact event, owner and reviewer null, state, `expectedFlowRevision`, non-empty note, no work evidence) and reach it only through `Tasks.returnUntouchedToBacklog` (#481 S9).
|
|
57
|
+
- The main-checkout note (`mainCheckoutWarning`, `src/hub/main-checkout.ts`, #572) is advisory text in the `hub_task_done` result, the review request and status, never a refusal. A branch name or other metadata the hub reads from outside the board reaches task text only after `Tasks.publicMetadataText` (PII patterns, secret shapes, private paths) and a plain ref-name check; otherwise use the generic sentence that carries none of it.
|
|
@@ -4,7 +4,7 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
|
|
|
4
4
|
|
|
5
5
|
- Tests that are not about batching build the bus with `batchMs: 0`; with the default 15 s window a lone status envelope looks like a lost message.
|
|
6
6
|
- In Bun 1.3.14 a test timeout that fires while `Bun.spawnSync` runs can start the next test inside spawnSync's own event loop, where another spawnSync then spins at full CPU for good (trigger not pinned down; see README). Bun 1.4.2 still runs the next test inside the outer spawnSync's event loop. `scripts/check.sh` runs tests with a 20 s timeout and under `scripts/hang-watch.sh`; a test that spawns and reads the process table gets a timeout of its own.
|
|
7
|
-
-
|
|
7
|
+
- Sandboxed commands and the MLX server get a fixed environment (`PATH`, `HOME` and their own keys), so test knobs for their fakes travel in a wrapper script or argv, not in `process.env`; native peer children inherit the hub's environment minus the agent markers (`peerChildEnv`).
|
|
8
8
|
- Bind agent identity markers explicitly in native/CLI fixture children. The full gate starts the test runner without inherited agent markers (including `AGENTHUB_DELIVERY_ONLY`) and without an `ahub claude` session's hub variables (`AGENTHUB_PROJECT_DIR`, `AGENTHUB_STATE_DIR`, `AGENTHUB_INSTANCE_ID`, `AGENTHUB_LAUNCH_ID`); a new variable a native session exports and a fixture child reads joins that `env -u` list in `scripts/check.sh`. Never weaken production marker detection to make them pass.
|
|
9
9
|
- `bunfig.toml` preloads `test/hub-home.ts`, which first clears inherited `AMEM_HOME` and gives every test run (the gate and `bun test <file>` alike) a temp `AGENTHUB_HOME` unless one is set; scoped tests may set their own `AMEM_HOME` after preload. Hub memory (#258) is on with any project config, and no test may write the person's `~/.agenthub`. Do not remove it; a test that needs its own home sets and restores the variable.
|
|
10
10
|
- `test/registry-guard.test.ts` statically scans `src/` against the committed `test/registry-guard.baseline.json` (#451); a test never writes, regenerates or repairs that baseline, and a failing guard is fixed in the source by moving the policy into a definition (rule: `docs/agent-notes/adapters.md`); the baseline stays `[]`. Scanner behavior is pinned on inline sources in `test/registry-guard-fixtures.test.ts`, never on edits to real files.
|
package/docs/memory.md
CHANGED
|
@@ -12,7 +12,7 @@ The [agent-mem repository](https://github.com/STAIxBWLB/agent-mem) is the refere
|
|
|
12
12
|
|
|
13
13
|
The memory root `M` is independent of the project state directory. The package's `M/profile.json` selects shared v1 or an explicit generation `G`; an absent selector means shared v1. The hub opens the selected profile with its current privacy screen and configured TTL. A generation uses `M/generations/G`, reads legacy prefixes through the package's readonly bridge, and writes its own rows and sync history there. The bridge does not migrate legacy bytes or use the legacy index. Exact-entry prune remains the package's person-only operation; it adds no shared-v1 operation and gives hub tools no prune authority.
|
|
14
14
|
|
|
15
|
-
A curated split (agent-mem#15 T2, `amem migrate --shards`) writes `M/shards.json`. Once it is active, the hub opens memory through the package's `openShardStore` for the project's key: `memoryProjectKey` of the hub's project, resolved through the person-reviewed alias log of the memory home, the same key the `amem` CLI binds. The daemon and `ahub memory`/`ahub goal` read and write under that key, split or not. A running hub resolves the key once at start; when `amem alias add` later maps that key elsewhere, its binding latches unavailable at the next memory entry, and a restart binds the new key. That store is this project's shard, with the user shard as its overlay. Other projects' private notes are not in it. A project first seen after the split gets its own shard on first open. The binding compares the shard identity (key, sealed generation, selector revision) at every entry. A split marker that is not active (captured, malformed, or a dangling or unreadable `shards.json`) refuses with `amem migrate --shards --resume` as the remedy and is never read as an empty or legacy store. A marker that appears under an open unsplit binding latches that binding unavailable. Without a marker, the hub opens memory exactly as before. `ahub memory profile-check` is
|
|
15
|
+
A curated split (agent-mem#15 T2, `amem migrate --shards`) writes `M/shards.json`. Once it is active, the hub opens memory through the package's `openShardStore` for the project's key: `memoryProjectKey` of the hub's project, resolved through the person-reviewed alias log of the memory home, the same key the `amem` CLI binds. The daemon and `ahub memory`/`ahub goal` read and write under that key, split or not. A running hub resolves the key once at start; when `amem alias add` later maps that key elsewhere, its binding latches unavailable at the next memory entry, and a restart binds the new key. That store is this project's shard, with the user shard as its overlay. Other projects' private notes are not in it. A project first seen after the split gets its own shard on first open. The binding compares the shard identity (key, sealed generation, selector revision) at every entry. A split marker that is not active (captured, malformed, or a dangling or unreadable `shards.json`) refuses with `amem migrate --shards --resume` as the remedy and is never read as an empty or legacy store. A marker that appears under an open unsplit binding latches that binding unavailable. Without a marker, the hub opens memory exactly as before. `ahub memory profile-check` is split-aware: on a split home it compares each running hub with the shard profile of that hub's own project (alias-resolved key, sealed generation, selector revision) without opening a store, and an inactive split reports the `amem migrate --shards --resume` remedy and exits 1 (#536).
|
|
16
16
|
|
|
17
17
|
All hub consumers share that binding: Tasks notes and lifecycle operations, leases, suggestions, briefs, recall, peer read tools, dashboard, ask and automatic sync. Standalone `amem` and `ahub memory`/`ahub goal` must resolve the same root and profile to see one another's writes. An invalid selector or missing/corrupt generation disables native memory without taking down the bus or task board; it never silently opens shared v1. The daemon reports its package/profile binding and availability separately from ordinary hub health.
|
|
18
18
|
|