@staix/agent-hub 0.12.19 → 0.12.20
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 +16 -0
- package/README.md +3 -3
- package/docs/agent-notes/adapters.md +1 -0
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +2 -1
- package/docs/agent-notes/models.md +3 -0
- package/docs/agent-notes/tasks.md +3 -2
- package/docs/events.md +4 -1
- package/docs/operations.md +413 -17
- package/docs/quickstart.md +1 -1
- package/docs/security.md +17 -10
- package/docs/smoke.md +58 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +399 -3
- package/docs/specs/2026-09-20-pi-local-models-design.md +2 -0
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +287 -0
- package/docs/specs/2026-10-04-switchyard-source-port-design.md +10 -0
- package/docs/verified.json +13 -13
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +63 -25
- package/src/adapters/claude-channel.ts +61 -20
- package/src/adapters/codex-appserver.ts +3 -0
- package/src/adapters/local-worker.ts +4 -0
- package/src/cli/console-state.ts +325 -102
- package/src/cli/console.ts +44 -23
- package/src/cli/help.ts +120 -0
- package/src/cli/identity.ts +3 -3
- package/src/cli/launch.ts +2 -0
- package/src/cli/main.ts +171 -75
- package/src/cli/preview.ts +1 -0
- package/src/cli/reset.ts +194 -0
- package/src/cli/status-lines.ts +9 -5
- package/src/cli/terminal-recovery.ts +140 -36
- package/src/cli/upgrade-runtime.ts +274 -56
- package/src/cli/upgrade.ts +366 -41
- package/src/hub/ask.ts +1 -1
- package/src/hub/board.ts +15 -7
- package/src/hub/bus.ts +60 -4
- package/src/hub/child-process.ts +1 -0
- package/src/hub/conductor.ts +38 -6
- package/src/hub/control-client.ts +5 -3
- package/src/hub/daemon.ts +56 -9
- package/src/hub/events.ts +7 -1
- package/src/hub/execution-budget.ts +15 -6
- package/src/hub/hub-tools.ts +4 -4
- package/src/hub/inference.ts +136 -1
- package/src/hub/lifecycle.ts +7 -1
- package/src/hub/peers.ts +2 -0
- package/src/hub/recovery-store.ts +51 -3
- package/src/hub/report.ts +43 -1
- package/src/hub/restart.ts +25 -0
- package/src/hub/routing.ts +48 -16
- package/src/hub/snapshots.ts +3 -2
- package/src/hub/tasks.ts +236 -83
- package/src/local/deny.ts +2 -1
- package/src/models/mlx.ts +11 -8
- package/src/models/ollama.ts +2 -2
- package/src/models/relay.ts +190 -42
- package/src/models/route/relay-selector.ts +43 -31
- package/src/models/route/runtime.ts +17 -7
- package/src/models/route/signals.ts +12 -1
- package/src/models/route/stage.ts +57 -3
- package/src/omniroute/client.ts +1 -1
- package/src/ui/index.html +1 -0
- package/templates/AGENTS.block.md +1 -1
- package/templates/routing.toml +8 -0
package/docs/security.md
CHANGED
|
@@ -4,7 +4,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
4
4
|
|
|
5
5
|
## Trust boundaries
|
|
6
6
|
|
|
7
|
-
- **Other agents' text is untrusted.** Every message that crosses from one peer to another is framed as untrusted input
|
|
7
|
+
- **Other agents' text is untrusted.** Every message that crosses from one peer to another is framed as untrusted input: a channel tag with `meta.source` for Claude started with `ahub claude`; otherwise a fixed header line (`frame()`, `[agent-hub message from ...]`) with an instruction that says it is untrusted, the plugin's server instructions for a tools-only Claude reading `hub_inbox` and a standing instruction for the others. A message body cannot forge the hub's own headers: such lines are quoted (`sanitize`). Replies inherit a hop count capped at 3, so agents cannot ping-pong forever; neither a digest nor a steer can reset it.
|
|
8
8
|
- **The control link is loopback plus a secret.** The daemon and the Codex proxy bind 127.0.0.1 only. The control WebSocket requires a per-run token (`.agenthub/state/control-token`, mode 600), and both servers refuse any request that carries an `Origin` header: any web page can open a WebSocket to localhost, and browsers always send `Origin`. External clients cannot claim the console user's id or a hub-managed peer's id.
|
|
9
9
|
- **Permission prompts stay on.** `ahub claude` and `ahub codex` add nothing that weakens the agents' own prompts. Kimi's and `local`'s permission requests are relayed to the console and cancelled after `approvals.timeout_s` (default 120 s) of silence; the macOS notification for a waiting request carries the peer and the tool name only. The one exception is the hub's own tools (`hub_send` and the task tools, matched by exact name): Kimi's requests for them are approved once without a prompt and logged by name, the same trust Codex gets through `approval_mode` in the hub's config. They invoke hub-owned operations rather than arbitrary file or shell tools, and every call passes the hub's own checks. Identity comes from the exact permission title, or from the earlier tool-call title bound to the same call id and resolved against the configured MCP servers when the permission title contains argument JSON (Qwen). Payload text and unrelated display titles never establish identity. For ACP agents, a payload longer than the console shows is marked as cut and never offers a session-wide grant. Pi's write, edit, shell and git write requests are relayed the same way and can be answered `Always allow <tool> until Pi restarts`: later calls of that tool by the same Pi start skip the prompt but stay inside the path guard and sandbox. The grant covers every later call of that tool whatever its payload, is kept in memory only, cannot be given from the dashboard, and each call it allows is logged by tool name. `--unattended` turns prompts off, says so loudly, and is never the default.
|
|
10
10
|
- **A committed config cannot choose launch commands, credential files, data endpoints or a wider sandbox.** The machine-local fields (`kimi_cmd`, `codex_bin`, `pi.cmd`, `checks`, `mlx.bin`, `mlx.runtimeDir`, `mlx.modelPath`, `omniroute.urls`, `omniroute.access_hosts`, the `omniroute` key files, `memory.worker_url`, `local.read_allow`, `local.bash_network`, `local.network_allow`) apply only from a config file git confirms nobody committed: `.agenthub/config.json` or `.agenthub/config.local.json`, matched by file identity so no other spelling the file system accepts slips past, and `.agenthub` itself not a committed symlink or submodule. Without a repository, or when git fails, they keep their defaults; an empty value always means the default. "Untracked" is answered by the repository that contains the project: a checkout copied or extracted into an unrelated repository, or into an ignored directory of one, is trusted like your own files. So a cloned repository cannot choose a launch command, a completion check, a gateway to send a key file to, a memory endpoint, or a wider sandbox. A command in `checks` runs as you, outside the local worker's sandbox, like a git hook. Nothing in `routing.toml` or in task text is ever run. `routing.toml` and the other shared fields still come from the checkout, and they matter: `routing.toml` picks the models the local worker and the hub's inference use at your gateway and can turn the PII constraint off, and roles and budget shape who does what. Review them in a repository you do not trust.
|
|
@@ -13,12 +13,13 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
13
13
|
- **The edit hook reads, never decides.** `ahub check-path --hook` (issue #32) reads hub.db and returns context for Claude and a line for you; it sets no permission decision, so your permission rules stay in charge. It names other owners' task ids, titles and states, which then reach Claude's model; PII tasks are left out.
|
|
14
14
|
- **The session record holds identities only.** `.agenthub/state/sessions.json` (issue #37, mode 600) keeps each attached peer's recovery metadata: launch options, session and thread ids, Pi's session file path. No message or task text; loss notices name deliveries by id, sender and public task title.
|
|
15
15
|
- **Context checkpoints have a separate text record.** Native occupancy readings carry numeric metadata only. An optional context checkpoint persists its summary in `.agenthub/state/context-checkpoint-<peer>.json` with mode 0600 and attempts shared-memory storage when enabled. Completion requires the outstanding request id, current peer/native session and transport generation; new connection claims invalidate old requests. Private turns, every open PII task associated with the peer as owner or reviewer (including pending review), and PII-pattern summaries are refused before persistence or cloud memory. Its text is never broadcast, and context pressure does not pause, reassign or replace the session.
|
|
16
|
-
- **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval, and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git/hooks` or `.git/config` writes; no network, loopback included, unless `local.bash_network`; with it, only through the hub's egress proxy to the hosts in `local.network_allow` (issue #65), which refuses names that resolve to internal addresses (behind NAT64 with the well-known prefix, by the IPv4 address the answer carries; a network-specific prefix is not recognised), so claude-mem and the Codex app-server stay out of reach (`"direct"` opens everything, until 0.13.0). One channel stays open: `trustd`, which TLS clients need, can fetch a certificate's AIA or OCSP URL on a command's behalf, outside the proxy. The allow-default profile of 0.9 and earlier was removed in 0.12.0 (issue #83); a `local.sandbox` setting is ignored with a note. The shared temp dirs (the user's and `/private/tmp`) are closed. Without the sandbox there is no `bash` tool.
|
|
16
|
+
- **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state and the state archives `ahub reset --all` leaves) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval, and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git/hooks` or `.git/config` writes; no network, loopback included, unless `local.bash_network`; with it, only through the hub's egress proxy to the hosts in `local.network_allow` (issue #65), which refuses names that resolve to internal addresses (behind NAT64 with the well-known prefix, by the IPv4 address the answer carries; a network-specific prefix is not recognised), so claude-mem and the Codex app-server stay out of reach (`"direct"` opens everything, until 0.13.0). One channel stays open: `trustd`, which TLS clients need, can fetch a certificate's AIA or OCSP URL on a command's behalf, outside the proxy. The allow-default profile of 0.9 and earlier was removed in 0.12.0 (issue #83); a `local.sandbox` setting is ignored with a note. The shared temp dirs (the user's and `/private/tmp`) are closed. Without the sandbox there is no `bash` tool.
|
|
17
17
|
- **Capabilities are enforced, not suggested.** `capabilities` (issue #39) is checked by the daemon where task operations and messages arrive, so a peer cannot get round it by phrasing. A peer can never answer a permission request: the control link takes `permit` from the console role only, and a message that quotes a permit command is just text. Both bind the hub's own tool paths: a vendor agent with its own shell in the project (Codex, Claude, Kimi) can read `.agenthub/state/control-token` and connect as the console, which only the local worker's and Pi's sandbox prevents.
|
|
18
18
|
- **PII has an enforced path.** A task matching `signals.pii_patterns` goes to `local` or to nobody; its text is absent from other peers' envelopes, the console stream, the log and the board listing; the console user reviews it; `local` answers such a turn to the console only, keeps it out of its history, refuses it when the only gateway is off campus, and may not save notes or spin off tasks during it. Nothing about it is sent to claude-mem, whose observer is a cloud model.
|
|
19
|
-
- **
|
|
19
|
+
- **A model screen can add to the patterns (issue #198).** With `signals.pii_screen = "local"` in `routing.toml` (default `off`; it applies while `constraints.pii = "local_only"`), a newly proposed task is read by a model before the hub does anything else with it: until the verdict it is on no board, in no envelope, before no triage model, and the hub makes no brief or note call for it. The proposer's own channel is not covered: when the local worker proposes it in an ordinary turn, claude-mem capture of that worker's tool calls (below) sends the arguments whatever the verdict. The screen runs on the model on this machine (MLX or Ollama, loopback only) or, without one, on the gateway only while `onCampus()` holds, and the gateway call is refused once more right before transport if the gateway is behind Access; task text never goes off campus to be screened. When the on-device generation slot is taken (Pi generating, another screen), the screen goes to the campus gateway instead of waiting, and on campus a device that fails or is still loading after 60% of the deadline hands over to the gateway too; off campus it waits for the slot and the device within the deadline. With the legacy MLX provider the screen may start an `mlx_lm.server`, as Pi's relay does, and it stays running until `ahub models stop`. Its answer is `pii` or `clear` plus a category from a closed list (name, student id, phone, address, grade, health, other). Anything else, a timeout (8 s, one call and one generation slot per item), a missing or off-campus model, or text longer than 6000 UTF-8 bytes (about 2000 Hangul syllables) is `unknown`, and an unknown task is a PII task: this is the hub's one model call that fails closed, so with the screener down every new task waits for `local`. An unknown verdict has the costs of any PII task, even when a second look clears it later: opening it lifts every silent turn-free cohort for good and turns facts off while it is open, and off campus a proposal without a class is refused (triage is not asked for a PII task there), with an error that says so. Off campus with the on-device slot taken, a task stays `unknown` until it is screened again: while it is still proposed and no peer ever owned it, the hub's release timer (every 60 s, one task at a time, the least tried first, at most 10 times per task in a hub run; never text too long to read) asks again, and `clear` lifts its `pii` signal and routes it through the class peers (a named owner is not remembered; a reserved one is offered first, as always). A task `local` took keeps its PII path, also once it is declined back to nobody. A pattern match is PII without a call. The task's history records the source of its `pii` signal (`regex`, `screen` with the category, or `unknown` with the reason), an unknown verdict is a console line naming the task id only, and `events.jsonl` gets a `pii_screen` record with the item kind, label, source, category, reason and latency, never the text. A routing explanation of a draft does not call the screen and says so.
|
|
20
|
+
- **Free text is screened too.** On an ordinary task, a done summary (with its check output: a match withholds the whole note), a review note, an unmet item or a budget handoff that matches a PII pattern (issue #69) is not saved to claude-mem, and every peer, `local` included, gets a stub naming `ahub task show <id>`; the board keeps the text, `hub.log` notes the withholding by task id only, and `ahub ask` shows such a note only when its model is reached on campus. `hub_remember` refuses a match; what a vendor agent writes to its own memory is not screened. With the model screen on, the same items (and `hub_remember` notes) go through it as well, one call each and only after the call's own checks (owner, state, verdict, a running check, memory enabled) pass: a `pii` or `unknown` verdict withholds the item exactly like a pattern match, and the board marks the history entry (`withheld`) so later views (task lists, completed-change notices, `ahub ask`, a reviewer replaced after a restart) keep withholding it. A budget hand-off is screened once per hand-off, whatever the number of tasks it moves; an escalation's reason is the hub's own text and is not screened. Pattern-only, without the model: the check output tail in a done note (only the summary is screened), a plan given with `hub_task_accept`, a decline reason, a task's refs (paths, branch), and context checkpoint summaries. Not screened at all (an existing gap, not new with the screen): the local worker's claude-mem capture records its tool calls in ordinary turns with their raw arguments, including the hub tools, so a title, detail, done summary, review note or `hub_remember` text it writes reaches claude-mem without a pattern or model check.
|
|
20
21
|
- **Secrets stay where they are read.** The gateway key and Cloudflare Access values are read inside the gateway client and go only into request headers: never into logs, errors, envelopes, tool output, memory, or the generated Switchyard config (the key travels by environment variable name). Subscription logins of Claude, Codex and Kimi are never proxied or pooled.
|
|
21
|
-
- **The hub's own model calls are fenced.** Digest condensation
|
|
22
|
+
- **The hub's own model calls are fenced.** Digest condensation, task triage and the PII screen read agent-written text as data; their output is capped text framed as untrusted, or a value checked against a closed list. It is never used as a route, a peer id, a tool call or an instruction.
|
|
22
23
|
|
|
23
24
|
## What is not defended
|
|
24
25
|
|
|
@@ -48,12 +49,18 @@ It does not make the token inaccessible to an agent with unrestricted project
|
|
|
48
49
|
shell access. The existing OS sandbox and loopback authentication boundaries apply.
|
|
49
50
|
|
|
50
51
|
Steering tools require an explicit `conductor` role, independent of default-allow
|
|
51
|
-
capabilities. Only one peer may hold that role.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
capabilities. Only one peer may hold that role. Two task-scoped exceptions need
|
|
53
|
+
no role: a task's current owner and reviewer may read its public view with
|
|
54
|
+
`hub_task_show`, and the peer that proposed a task may redirect it with
|
|
55
|
+
`hub_task_assign` while it is proposed, nobody ever accepted it and its latest
|
|
56
|
+
move other than the hub's own was not made from the console (to another peer
|
|
57
|
+
only with `assign` when its capabilities are listed).
|
|
58
|
+
A conductor may inspect public state, assign or escalate work, start supported
|
|
59
|
+
headless peers and place its own delivery holds. It cannot answer approvals,
|
|
60
|
+
resolve durable deliveries, override budget pauses or release a human hold.
|
|
61
|
+
Pending approval summaries exclude titles; PII tasks remain public stubs.
|
|
62
|
+
Conduct events contain ids only. Supervision feeds use structured reasons and
|
|
63
|
+
never carry check output or approval bodies.
|
|
57
64
|
|
|
58
65
|
## Reporting
|
|
59
66
|
|
package/docs/smoke.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
`scripts/check.sh` covers everything against fakes. The legs below need real accounts and an interactive terminal, so they are run by hand and recorded here.
|
|
4
4
|
|
|
5
|
+
## ahub reset on a scratch project (#214)
|
|
6
|
+
|
|
7
|
+
Not yet run. A person runs it in a terminal, on a scratch project only, never
|
|
8
|
+
on a project whose hub state matters:
|
|
9
|
+
|
|
10
|
+
1. `ahub init`, `ahub up`, attach Claude with `ahub claude` and one headless
|
|
11
|
+
peer; queue work for an offline peer with `ahub say @<peer> ...` and
|
|
12
|
+
`ahub pause <peer>`; propose one task.
|
|
13
|
+
2. Quit Claude Code (its status line tee writes into the state directory) and
|
|
14
|
+
run `ahub kill`, so nothing else changes the state files, and take their
|
|
15
|
+
checksums. `ahub reset`: the listing shows ids and counts only, and
|
|
16
|
+
`hub.db`, `hub.db-wal` and the other state files keep their checksums
|
|
17
|
+
(`hub.db-shm`, SQLite's shared-memory index, may change on any read).
|
|
18
|
+
3. `ahub up` and `ahub claude` again, then `ahub reset --yes`: the hub stops
|
|
19
|
+
first; `ahub up` then shows no queued or needs_review delivery
|
|
20
|
+
(`ahub queue list`), no hold or pause, and the same board; the reset says
|
|
21
|
+
to relaunch Claude with `ahub claude`.
|
|
22
|
+
4. `ahub reset --all --yes`: `.agenthub/archive/state-<UTC time>/` is 0700 and
|
|
23
|
+
git-ignored, the state directory holds only `project.json`, and `ahub up`
|
|
24
|
+
starts with the same project id, an empty board and an empty queue.
|
|
25
|
+
5. Restore the archive as `docs/operations.md` describes (the current state
|
|
26
|
+
directory goes into `.agenthub/archive/` too) and check the board from
|
|
27
|
+
step 1 is back.
|
|
28
|
+
|
|
29
|
+
Record the version, the commands, each result and anything that differed.
|
|
30
|
+
|
|
5
31
|
## Operator console and conductor candidate (#190, #191, #193-#195)
|
|
6
32
|
|
|
7
33
|
Candidate 0.12.17, protocol 15, observed on 2026-10-09 KST. The final Claude
|
|
@@ -321,6 +347,23 @@ ahub setup # Claude Code channel plugin from thi
|
|
|
321
347
|
|
|
322
348
|
Claude channels are a research preview: `ahub claude` passes `--dangerously-load-development-channels plugin:agent-hub@agent-hub`.
|
|
323
349
|
|
|
350
|
+
## Claude without channel evidence (#205)
|
|
351
|
+
|
|
352
|
+
Not run live yet. On a candidate with the rebuilt plugin bundle (protocol 16),
|
|
353
|
+
record only what was observed:
|
|
354
|
+
|
|
355
|
+
1. Start a plain `claude` in the project (not `ahub claude`). `ahub status`
|
|
356
|
+
shows `claude` `idle` with `tools-only: messages wait for hub_inbox; ...`.
|
|
357
|
+
Run `ahub say @claude "<unique text>"`: status shows `queued 1`, `ahub queue
|
|
358
|
+
list --peer claude` has no `accepted` or `dispatching` row, and no pushed row
|
|
359
|
+
appears in the TUI. Ask the session to call `hub_inbox`: it returns the text
|
|
360
|
+
under an `[agent-hub message from "user"` header, status shows `queued 0`,
|
|
361
|
+
and the queue list shows one `completed` row `read through hub_inbox`.
|
|
362
|
+
2. Start `ahub claude` in the same project (AC3). It takes the peer and the
|
|
363
|
+
plain session stands by. A new `ahub say @claude "<unique text>"` appears as
|
|
364
|
+
a native channel row, its delivery reaches `accepted`, and `hub_delivery_done`
|
|
365
|
+
or a correlated reply settles it `completed`. Status shows no `tools-only`.
|
|
366
|
+
|
|
324
367
|
## npm package preparation (issue #4)
|
|
325
368
|
|
|
326
369
|
Verified locally on 2026-09-19 with Bun 1.3.14, using an actual `npm pack` tarball:
|
|
@@ -1285,3 +1328,18 @@ refused by the identity gate). Claude (the session writing this) and Pi on
|
|
|
1285
1328
|
nothing happened on the hub: plain `ahub console`, 0.11 s over 60 s
|
|
1286
1329
|
(about 0.18%); `ahub console --panels`, 0.46 s over 120 s from 05:52:30
|
|
1287
1330
|
(about 0.38%, 0.21 to 0.25 s per minute, not growing).
|
|
1331
|
+
|
|
1332
|
+
## Console layout and readability: how to check (#213, #201 AC2)
|
|
1333
|
+
|
|
1334
|
+
Not run yet. Record the terminal, its theme and the result per step.
|
|
1335
|
+
|
|
1336
|
+
- In a light and a dark terminal profile, run `ahub console --panels` at 80x24
|
|
1337
|
+
with at least one pending approval, one task and one `needs_review` delivery.
|
|
1338
|
+
- Check that the rules, the bracketed active tab, the tab counts, every table's
|
|
1339
|
+
header row and the footer hint are readable, and that titles, approval details
|
|
1340
|
+
and body text use the default foreground.
|
|
1341
|
+
- Repeat with `--color=never`: the active tab, selection marker and states must
|
|
1342
|
+
still read without color.
|
|
1343
|
+
- Press `?`, Enter on an approval and Tab back to the stream; check that the key
|
|
1344
|
+
table, the labeled detail and the stream's rule above the footer read in both
|
|
1345
|
+
themes.
|
|
@@ -986,8 +986,8 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
986
986
|
route explain` and assignment agree. Accept and done refuse a waiting task,
|
|
987
987
|
for the console user too.
|
|
988
988
|
- A proposal that names an owner (a claim, or an owner for someone else) and
|
|
989
|
-
would wait
|
|
990
|
-
|
|
989
|
+
would wait stores that owner as the task's reserved owner (issue #207 below
|
|
990
|
+
replaced the refusal this bullet first specified).
|
|
991
991
|
- Approval, by review or by a class without a reviewer, releases the
|
|
992
992
|
dependents that wait for nothing else; each is recorded as `ready` and assigned.
|
|
993
993
|
A proposal re-reads what it waits for after its insert, since triage may have
|
|
@@ -1377,13 +1377,66 @@ checks even for redirected stream output, without enabling panels or raw mode;
|
|
|
1377
1377
|
never disables styling. Invalid values fail with usage text before connection.
|
|
1378
1378
|
Plain `renderConsole` remains available, while `renderConsoleLines` exposes
|
|
1379
1379
|
semantic spans. Geometry is computed from sanitized plain Unicode text before
|
|
1380
|
-
painting.
|
|
1380
|
+
painting. Wrapping breaks at whitespace and splits only a word longer than a whole
|
|
1381
|
+
line; a wrapped line's continuations start four columns deeper than
|
|
1382
|
+
its source line; a source indent and a continuation indent are both capped at half the width, so wrapped peer text in the stream
|
|
1383
|
+
never starts at a column where hub-written lines start (#212). `paint` sanitizes every span, inserts only fixed palette SGR sequences
|
|
1381
1384
|
and resets each styled span. Interactive exits, signals, disconnect and errors
|
|
1382
1385
|
retain the console restoration sequence. No new dependencies, protocol changes,
|
|
1383
1386
|
polling or redraw triggers are added. Tail, logs and machine-readable output
|
|
1384
1387
|
remain unchanged. Native light/dark readability and idle-CPU observations are
|
|
1385
1388
|
separate smoke evidence, not inferred from fake-terminal tests.
|
|
1386
1389
|
|
|
1390
|
+
Console layout (issue #213) separates the panel header (the project root's
|
|
1391
|
+
directory name, since the registry id is a hash, then the tabs), the body and
|
|
1392
|
+
the footer with ASCII `-` rules, and puts one rule above the stream footer; the
|
|
1393
|
+
console emits no box-drawing or other ambiguous-width glyph. The active tab is
|
|
1394
|
+
bracketed; the Approvals and Queue tabs count pending requests and
|
|
1395
|
+
`needs_review` deliveries. Peers, Approvals, Tasks and Queue are tables: a
|
|
1396
|
+
header row, columns two spaces apart sized with `Bun.stringWidth` from every
|
|
1397
|
+
row (at most a third of the width), the last column taking the rest. Peers' LINK
|
|
1398
|
+
column reads `tools-only` when the optional `toolsOnly` status field (#205) is
|
|
1399
|
+
present. Zero counters read `-`, durations `45s`, `12m`, `3h05m` or `2d03h`, and
|
|
1400
|
+
resets and pauses are relative. Detail views and the Approvals panel's request
|
|
1401
|
+
block put labels at column 0 and values one column past the longest label; a
|
|
1402
|
+
list puts `- ` before each item, and every other line of a value (its own
|
|
1403
|
+
newlines, wrapped continuations) starts two columns deeper, so untrusted text
|
|
1404
|
+
passes neither for a label nor for an item. Only a field's own string shows as
|
|
1405
|
+
written: every agent-written string below it (nested pairs and lists, list
|
|
1406
|
+
items) and every option name or id in the Approvals block, the prompts and the
|
|
1407
|
+
stream's permission line is JSON-quoted unless it is a plain word starting with
|
|
1408
|
+
a letter, so `;`,
|
|
1409
|
+
`,`, `)`, quotes and newlines in it cannot pass for hub structure. Values show
|
|
1410
|
+
no JSON objects, epoch milliseconds or ISO times. Allow options keep one line
|
|
1411
|
+
each; `d deny` always stays in view, options that do not fit are counted on a
|
|
1412
|
+
`(more)` line, table rows give way, and a cut title is marked `(more)` in the
|
|
1413
|
+
label column. The selected request is tracked by id: only the first request is
|
|
1414
|
+
selected on arrival, `[`, `]` and j/k move the selection, `a`, `d` and `v` act on
|
|
1415
|
+
it alone, and its detail is rendered from the live request. When it closes, the
|
|
1416
|
+
selection and its detail clear with a notice; `a`, `d` and `v` act on nothing
|
|
1417
|
+
until `[`, `]` or j/k select again, so a request that arrives as the person
|
|
1418
|
+
reacts is never acted on. Everything pending on a request belongs to the
|
|
1419
|
+
selected one, checked after every key and every prune: an option choice or
|
|
1420
|
+
confirmation for another request is dropped, an open detail follows the
|
|
1421
|
+
selection from its top, and the option prompt names the request. `a`, `d` and
|
|
1422
|
+
`v` act only while the request list or its detail is shown, and a task or
|
|
1423
|
+
delivery detail that arrives after the person moved to another panel or row is
|
|
1424
|
+
dropped; with a task, delivery or peer detail open, the row keys act on the item
|
|
1425
|
+
the detail shows, by its id. A pushed request's agent-supplied fields are kept as
|
|
1426
|
+
strings, so a malformed one cannot throw on a draw. The key table is modal: only
|
|
1427
|
+
`?`, Escape and `q` act under it, and a paste is dropped. Offline peers keep the default foreground, also
|
|
1428
|
+
when selected, and a tab counts as one space when a line is cut. The footer
|
|
1429
|
+
hint lists only the keys valid in the mode, panel and state, and `?` shows a
|
|
1430
|
+
key table grouped by panel (in stream mode it is printed into the stream, as
|
|
1431
|
+
`v`'s copy of a request is, without entering the Events buffer). Errors and refusals take the failure tone, informing notices (a closed or
|
|
1432
|
+
missing selection) the attention tone. A notice clears at the next key, or once 10 s have passed when the
|
|
1433
|
+
existing 1 s tick redraws. In the stream, `v` prints the selected request with
|
|
1434
|
+
the push's framing, and `:` command output lines start at column 4. `fit`
|
|
1435
|
+
measures a cut line as `paint` prints it, so a cut that ends a span in
|
|
1436
|
+
`[agent-hub` cannot widen the line with a `> ` marker. A context reading in the
|
|
1437
|
+
console stream and Events panel shows its measurement time as local time, as
|
|
1438
|
+
event headers do; `ahub tail` and `ahub status` keep the ISO time.
|
|
1439
|
+
|
|
1387
1440
|
Agent shell CLI calls connect as the detected peer in tools mode. Hub launches
|
|
1388
1441
|
set `AGENTHUB_PEER_ID`; the pinned installed Codex shell injects
|
|
1389
1442
|
`CODEX_THREAD_ID` after environment filtering, and Claude uses `CLAUDECODE`.
|
|
@@ -1534,3 +1587,346 @@ only. The path guard, denylist and sandbox still apply to each call; the
|
|
|
1534
1587
|
dashboard still offers Pi only deny; unattended mode still picks `allow_once`;
|
|
1535
1588
|
the local worker keeps allow and deny. The console and `ahub permit` already
|
|
1536
1589
|
handle more than one allow option, so the control protocol is unchanged.
|
|
1590
|
+
|
|
1591
|
+
## Amendment: Claude sessions without channel evidence (issue #205)
|
|
1592
|
+
|
|
1593
|
+
Claude Code without `--dangerously-load-development-channels` drops channel
|
|
1594
|
+
notifications without an error, so a plain `claude` session used to mark every
|
|
1595
|
+
push `accepted` and show none of them.
|
|
1596
|
+
|
|
1597
|
+
- Evidence: `ahub claude` passes the flag and, in the same launch, sets
|
|
1598
|
+
`AGENTHUB_CHANNEL=1` in Claude's environment (`nativeLaunchEnv`);
|
|
1599
|
+
`peerChildEnv` removes the marker from every other native child. The plugin's
|
|
1600
|
+
MCP server inherits Claude Code's environment (a local check on 2026-10-09,
|
|
1601
|
+
not a live smoke: a plain session's plugin server inherited a shell-only
|
|
1602
|
+
`AGENTHUB_*` variable, and the plugin's `.mcp.json` sets no env), as it
|
|
1603
|
+
already does for
|
|
1604
|
+
`AGENTHUB_STATE_DIR` and `AGENTHUB_PEER_ID`. Only the value `1` counts. With
|
|
1605
|
+
the marker the server keeps
|
|
1606
|
+
the push and settlement contract of "Live channel settlement" unchanged.
|
|
1607
|
+
Without it (a plain `claude`, an IDE session, or the flag passed by hand) the
|
|
1608
|
+
session attaches tools-only.
|
|
1609
|
+
- Decision: the launch marker is sufficient evidence, and no positive readback
|
|
1610
|
+
is required before the first `accepted`.
|
|
1611
|
+
- The marker and the flag come from the same launch, so the marker shows that
|
|
1612
|
+
this Claude process got the flag. A readback would add per-push proof of
|
|
1613
|
+
display, which stays out of scope: `accepted` means the bridge handed the
|
|
1614
|
+
notification over, and `hub_delivery_done` or a correlated reply settles
|
|
1615
|
+
handled work.
|
|
1616
|
+
- A readback (a native turn citing the pushed id) would hold every first
|
|
1617
|
+
delivery until the model chose to answer, and a session that never answers
|
|
1618
|
+
would look the same as one that never rendered.
|
|
1619
|
+
- The existing launch identity (`AGENTHUB_INSTANCE_ID`, `AGENTHUB_LAUNCH_ID`)
|
|
1620
|
+
is not used: `ahub claude` sets it only when a daemon is already running at
|
|
1621
|
+
launch, so an `ahub claude` started before `ahub up` would lose pushes it
|
|
1622
|
+
can show.
|
|
1623
|
+
- Limits: the marker is inherited by the session's own shell children, so a
|
|
1624
|
+
plain `claude` started inside an `ahub claude` session reads as
|
|
1625
|
+
channel-capable (it also inherits the peer id and takes the peer over); so
|
|
1626
|
+
does `claude -p` run by a hook or script inside such a session, with its
|
|
1627
|
+
state dir and peer id. The marker is per process while the flag names MCP
|
|
1628
|
+
servers: `ahub claude --mcp-config <candidate>` without
|
|
1629
|
+
`--strict-mcp-config` loads channels only for `server:agent-hub`, yet the
|
|
1630
|
+
installed plugin server in the same process also sees
|
|
1631
|
+
`AGENTHUB_CHANNEL=1`. A session started with the flag by hand reads as
|
|
1632
|
+
tools-only and is pointed to `ahub claude`.
|
|
1633
|
+
- Tools-only attach: the server declares tools only, sends `channel: false` in
|
|
1634
|
+
its `peer` hello and lists `hub_inbox` but not `hub_delivery_done`. It never
|
|
1635
|
+
reports `accepted`: a `deliver` it is handed anyway goes back as a
|
|
1636
|
+
`needs_review` receipt. The daemon attaches its `WsPeer` as `pullOnly`; a
|
|
1637
|
+
push drain needs the peer deliverable (`Bus.deliverable`: not held, idle, not
|
|
1638
|
+
pull-only), checked at each loop and again after condensation, so a plain
|
|
1639
|
+
session that takes the peer while a push is condensed gets nothing. Its
|
|
1640
|
+
messages stay queued and count as `queued`. Board routing still treats the
|
|
1641
|
+
peer as attached.
|
|
1642
|
+
- `hub_inbox` sends `inbox`. The daemon answers "not attached yet; retry"
|
|
1643
|
+
between welcome and attach, and refuses it for a session with pushes.
|
|
1644
|
+
`Bus.pull` is held by every hold a drain respects (`Bus.held`: recovery, an
|
|
1645
|
+
uncertain delivery, a console, budget or conductor pause), though unlike a
|
|
1646
|
+
drain it does not wait for the peer to be idle; a held pull journals nothing
|
|
1647
|
+
and the refusal names the hold and the next action. Otherwise it takes what one
|
|
1648
|
+
push delivery would (the preface, then `take`'s batch of at most ten,
|
|
1649
|
+
important first; the reply says how many still wait) and checkpoints it like
|
|
1650
|
+
a push, so queued journal rows of the same envelopes (an operator retry) are
|
|
1651
|
+
grouped into it, then marks it `completed`, reason `read through hub_inbox`,
|
|
1652
|
+
in the same transaction: the tool result that returns them is the readback,
|
|
1653
|
+
so nothing waits in `accepted`. What it hands over is registered (and moved
|
|
1654
|
+
to the newest end of the cache) for `reply_to` like a push, and a read clears
|
|
1655
|
+
the peer's failure streak as a completed push does. If that journal write fails, only this peer's queue
|
|
1656
|
+
and preface are put back (every pause stays) and the bus stops with
|
|
1657
|
+
`delivery journal unavailable`. A pull is not a native turn and is not
|
|
1658
|
+
counted as supervision. The row is completed at hand-out, before anything
|
|
1659
|
+
shows the model saw it: a reply lost on the way, a tool call cancelled with
|
|
1660
|
+
Esc or a plugin that dies loses the batch (a `ponytail:` ceiling in
|
|
1661
|
+
`Bus.pull`), and `ahub queue resolve --action retry` refuses a terminal row.
|
|
1662
|
+
The completed row keeps the envelopes, so the operator reads them with `ahub
|
|
1663
|
+
queue show <id>` and sends what matters again.
|
|
1664
|
+
- Who holds the peer: an `ahub claude` session takes it from a plain session
|
|
1665
|
+
as any newer hello does, a standing-by one included (its standby check does
|
|
1666
|
+
not wait for a holder that status reports `toolsOnly`); attach clears
|
|
1667
|
+
`pullOnly`, the waiting queue is pushed to it, and the plain session stands by
|
|
1668
|
+
(close 4000). A plain session
|
|
1669
|
+
never takes it from a session with pushes, attached or still arriving: its
|
|
1670
|
+
hello is closed with 4000 and it stands by the same way, so an IDE or plain
|
|
1671
|
+
`claude` with the user-scope plugin cannot strand the channel session's
|
|
1672
|
+
unsettled deliveries. It attaches once that session leaves.
|
|
1673
|
+
- Status: `status.json` peers carry `toolsOnly`, a line naming the state and the
|
|
1674
|
+
next action (`ahub claude`); `ahub status`, the console footer and the
|
|
1675
|
+
dashboard show it.
|
|
1676
|
+
- Control protocol 16 adds the hello `channel` field and `inbox`. Recovery
|
|
1677
|
+
sources add protocol 15 (0.12.17 through 0.12.19); the transition from a real
|
|
1678
|
+
0.12.19 hub is not yet proven.
|
|
1679
|
+
## Amendment: owner and reviewer read their task (issue #208)
|
|
1680
|
+
|
|
1681
|
+
`hub_task_show` stays a conductor tool, and a task's current owner and current
|
|
1682
|
+
reviewer may also call it for that task. They get exactly the conductor's
|
|
1683
|
+
view: `publicConductorTask` over `Tasks.publicView(task, true)`, so the whole
|
|
1684
|
+
history with its done summary and check line, notes that match a PII pattern
|
|
1685
|
+
withheld, and a PII task as a stub with no history. Any other peer is refused
|
|
1686
|
+
with the conductor-role error, whatever the task. That refusal is about the
|
|
1687
|
+
tool, not a secret: the view holds nothing private beyond the PII stub, and the
|
|
1688
|
+
control op `task_show` (`ahub task show` from an agent shell) already returns
|
|
1689
|
+
the same public view to any peer. These reads write no conduct audit event, as
|
|
1690
|
+
task list polling does not. `hub_status` stays conductor-only.
|
|
1691
|
+
|
|
1692
|
+
## Amendment: reserved owners and proposer redirects (issue #207)
|
|
1693
|
+
|
|
1694
|
+
- `reserved` is a nullable text column on `tasks`, added to existing boards
|
|
1695
|
+
like `plan` and `deps` (old rows reserve nobody). `hub_task_propose` with an
|
|
1696
|
+
`owner` and an `after` that still waits stores the owner there instead of
|
|
1697
|
+
refusing; the capability check for naming another peer is unchanged. With
|
|
1698
|
+
nothing left to wait for, a named owner is assigned as before.
|
|
1699
|
+
- The reservation is an input of `assign()`, read from the task, so `ahub
|
|
1700
|
+
route explain` and assignment agree. When no candidate list is given, the
|
|
1701
|
+
reserved owner is the only candidate if `blocked()` passes it; otherwise the
|
|
1702
|
+
trace says why it is not honored and routing proceeds through the class
|
|
1703
|
+
peers. PII, capability limits (`local_allowed`, Pi's context limit),
|
|
1704
|
+
exclusions, failing peers and offline or paused peers all pass it over, so a
|
|
1705
|
+
PII task still goes to `local` or nobody.
|
|
1706
|
+
- Lasting exclusions are refusals only: a decline records the owner it was
|
|
1707
|
+
declined for in its history entry's `from` (also when the console declines
|
|
1708
|
+
for that owner), and an escalation with reason `rejections` (a reviewer's
|
|
1709
|
+
repeated changes_requested) or `manual` (by hand) records the owner it took
|
|
1710
|
+
the task from the same way. `excluded()` reads both (older decline rows name
|
|
1711
|
+
only the decliner), so those peers stay out of every later reroute and of
|
|
1712
|
+
`route explain`: the reservation never hands the task back to them. The
|
|
1713
|
+
hub's own escalations after a failed delivery or inference
|
|
1714
|
+
(`delivery_failed`, `inference_failed`), and an owner released as gone, moved
|
|
1715
|
+
by the idle sweep or relayed for its budget, exclude that owner only from
|
|
1716
|
+
that move's own reroute: none of these is a refusal, so a later reroute may
|
|
1717
|
+
give it the task again from its reservation. A person's explicit assign
|
|
1718
|
+
(`ahub task assign`, the dashboard) is not blocked by lasting exclusions and
|
|
1719
|
+
clears an agent's reservation, so a later reroute does not hand the task to
|
|
1720
|
+
the agent's choice; the conductor's and a proposer's assigns respect the
|
|
1721
|
+
exclusions and keep the reservation.
|
|
1722
|
+
- A named candidate list (console or conductor assign, escalation, budget
|
|
1723
|
+
relay, the idle sweep's auto-reassignment of its suggestion) replaces the
|
|
1724
|
+
reservation; the idle sweep's step-3 suggestion itself is an `assign()`
|
|
1725
|
+
without candidates, so it consults the reservation like routing does. A task
|
|
1726
|
+
that waits shows its reservation in the trace before the readiness check,
|
|
1727
|
+
which then stops; the trace says whether routing would honor it only once
|
|
1728
|
+
the task is ready. `route explain <id>` also names it in the header. The
|
|
1729
|
+
reservation stays after it is honored or passed over.
|
|
1730
|
+
- Passing over a reservation is a console and hub.log notice naming the
|
|
1731
|
+
reserved peer and the reason, from assignment and from the idle sweep's
|
|
1732
|
+
suggestion alike, and the assignment's history note keeps it, appended to a
|
|
1733
|
+
note the caller gave (a gone owner's release). Only news is said: nothing
|
|
1734
|
+
when the reserved peer is the owner the task moves away from (its decline,
|
|
1735
|
+
escalation, release or idle sweep), and nothing once it refused the task for
|
|
1736
|
+
good (the decline or escalation said that). The reserved peer is not
|
|
1737
|
+
messaged.
|
|
1738
|
+
- `Tasks.assignTo` on a task that waits records a `reserved` event with reason
|
|
1739
|
+
`manual` and changes the reserved owner instead of the silent no-op it was;
|
|
1740
|
+
this applies to the console, the conductor and a proposer alike. The release
|
|
1741
|
+
timer treats a `reserved` last event like `blocked`.
|
|
1742
|
+
- `hub_task_assign` stays a conductor tool, and the task's proposer (the `by`
|
|
1743
|
+
of its first history entry) may call it while the task is `proposed` and its
|
|
1744
|
+
history has no `accepted` entry: a decline or a release puts a task that was
|
|
1745
|
+
worked on back in `proposed`, and that task is no longer the proposer's to
|
|
1746
|
+
redirect. Handing it to another peer needs `assign` when the proposer has a
|
|
1747
|
+
capabilities entry; redirecting to itself does not. The conductor needs
|
|
1748
|
+
`assign` for any assign when it has a capabilities entry. The proposer path
|
|
1749
|
+
is refused while the last owner or reservation change of the task
|
|
1750
|
+
(`assigned`, `reassigned`, `escalated`, `unassigned`, `reserved`) that the
|
|
1751
|
+
hub did not make itself was made by the console user: a person's assign or
|
|
1752
|
+
reservation stands until the conductor or the console moves it, also after
|
|
1753
|
+
the hub carried it out or passed it over. The id is taken as `Tasks.need()`
|
|
1754
|
+
takes it, a whole number or a digit string. Any other non-conductor peer, and
|
|
1755
|
+
the proposer once the task was accepted or a person moved it, get the
|
|
1756
|
+
conductor-role error. A proposer's redirect goes through `Tasks`, keeps the
|
|
1757
|
+
proposer as the actor with reason `manual` (on the `reassigned` or `reserved`
|
|
1758
|
+
entry), and writes no conduct audit event; during a PII turn it is refused
|
|
1759
|
+
like the conductor's.
|
|
1760
|
+
## Amendment: ahub reset (issue #214)
|
|
1761
|
+
|
|
1762
|
+
`ahub reset [--all] [--yes]` returns a project's hub to a clean state. It is
|
|
1763
|
+
human-only (the CLI identity gate refuses it from an agent shell) and a dry run
|
|
1764
|
+
unless `--yes`: the dry run reads `hub.db` read-only (with `immutable=1` when
|
|
1765
|
+
`hub.db-wal` or `-shm` is absent, so it creates neither) and prints delivery ids,
|
|
1766
|
+
peer names and counts per category, never task or message text. With `--yes`
|
|
1767
|
+
it stops a running hub through `stopProject`, the path `ahub kill` takes, then
|
|
1768
|
+
acts on the stopped state directory.
|
|
1769
|
+
|
|
1770
|
+
- Refusals, each before anything changes: the machine's recovery lock is held
|
|
1771
|
+
(`recoveryLock()` read directly: a reset is never part of an operation, so
|
|
1772
|
+
`AGENTHUB_RECOVERY_OPERATION` does not let it through as it lets the
|
|
1773
|
+
operation's own processes through `assertLifecycleAvailable`); no
|
|
1774
|
+
registration matches this root and state directory, running or not (`ahub
|
|
1775
|
+
up` registers a stopped project; when this root is registered with another
|
|
1776
|
+
state directory the error names `ahub --project <id> reset` instead, since
|
|
1777
|
+
`ahub up` would register the default directory and orphan that state); the
|
|
1778
|
+
hub's ownership is not verified (any inspection state other than stopped, running
|
|
1779
|
+
or stopping); a running hub reports a recovery operation whose phase is not
|
|
1780
|
+
`released`; `--all` with a state directory other than the literal
|
|
1781
|
+
`<root>/.agenthub/state` (an override or symlink elsewhere would cross file
|
|
1782
|
+
systems or pull outside state into the tree); `--all` when `.agenthub` or
|
|
1783
|
+
the state directory is not a real directory of this user, when
|
|
1784
|
+
`.agenthub/archive` exists as anything else (a symlink could carry hub.db,
|
|
1785
|
+
PII included, out of the project), or when its `.gitignore` is not a regular
|
|
1786
|
+
file (a symlink would truncate the file it names). These are checked before
|
|
1787
|
+
the stop and again right before the rename (`archiveProblem`); a hard-linked
|
|
1788
|
+
`.gitignore` is replaced by a temp file and rename, never written through.
|
|
1789
|
+
Checked, then renamed by path: a symlink swapped in between still wins
|
|
1790
|
+
(marked `ponytail:`; `renameat` on a held descriptor would close it).
|
|
1791
|
+
- Between stop and act: after `stopProject` the CLI takes the project's
|
|
1792
|
+
registry claim (`Registry.claim` with its own pid), the claim a daemon must
|
|
1793
|
+
hold to run, and checks that no manifest names a daemon whose pid is alive
|
|
1794
|
+
or uncertain (`processAlive`, the rule `inspectProject` uses); either failing
|
|
1795
|
+
means a hub started in between and nothing is reset. A reset's claim carries
|
|
1796
|
+
the instance prefix `reset-`, so losing the claim to a concurrent reset says
|
|
1797
|
+
so. A crashed daemon's manifest (`status.json`, `control-token`, `hub.pid`
|
|
1798
|
+
with a dead pid, after SIGKILL or the forced shutdown exit; an empty or
|
|
1799
|
+
garbled `hub.pid` beside a dead `status.json` included) does not count: the runtime reset
|
|
1800
|
+
removes it and `--all` archives it. With the claim held, the CLI checks the
|
|
1801
|
+
machine's recovery lock again, so an upgrade or recovery that took it in
|
|
1802
|
+
between does not run beside the reset. The claim is released when the reset
|
|
1803
|
+
ends; a reset that dies leaves a dead pid, which the next claim takes over.
|
|
1804
|
+
- Failures: the plan is read only for the dry run, so `--all --yes` archives a
|
|
1805
|
+
state directory it cannot read as it is (its dry run says what it could not
|
|
1806
|
+
read). A failure after the stop says how far it got. A runtime reset that
|
|
1807
|
+
cannot read the state (a corrupt `hub.db`, malformed JSON or valid JSON of
|
|
1808
|
+
the wrong shape, which `planReset` checks as the journal would, a journal the
|
|
1809
|
+
constructor refuses: root mismatch, corrupt metadata, too many rows) fails
|
|
1810
|
+
the same way on a rerun, so its error points to `--all`; any other runtime
|
|
1811
|
+
failure is finished by a rerun, every step being idempotent. `--all` prints
|
|
1812
|
+
the archive path as soon as the rename happened; a failure before it says
|
|
1813
|
+
the hub is stopped and nothing was moved. JSON and SQLite errors are printed
|
|
1814
|
+
by class and code only (`failureText`): a parser's message can quote the
|
|
1815
|
+
data it choked on, envelope bodies and task text included.
|
|
1816
|
+
- Runtime scope (default): the CLI opens the journal as a new instance, which
|
|
1817
|
+
turns the stopped run's dispatching and accepted rows into `needs_review`,
|
|
1818
|
+
and settles every `queued` and `needs_review` entry of `Bus.queueList`,
|
|
1819
|
+
including queue entries without a row, with `resolveDelivery(..., "discard",
|
|
1820
|
+
"reset")`: one `resolution_history` entry each. It clears manual holds
|
|
1821
|
+
through the bus, deletes the `budget_pauses` and `conductor_holds` rows and
|
|
1822
|
+
removes `sessions.json`, `claude-session.json` and `claude-context.json`,
|
|
1823
|
+
and a crashed run's manifest.
|
|
1824
|
+
The board, turns and touches, execution budgets, logs, `events.jsonl`,
|
|
1825
|
+
`cli-audit/`, recovery records, `pi-sessions/` and configuration stay.
|
|
1826
|
+
- Full scope (`--all`): the state directory is renamed, unchanged, to
|
|
1827
|
+
`<root>/.agenthub/archive/state-<YYYYMMDDTHHMMSSZ>/` (0700); an existing
|
|
1828
|
+
target refuses the move. The archive directory gets a `.gitignore` of `*`
|
|
1829
|
+
(checked on every `--all` and rewritten when it differs) because `hub.db`
|
|
1830
|
+
holds task text, PII included; turn snapshots exclude `**/.agenthub/archive/**`
|
|
1831
|
+
as they exclude the state directory, also when the user's index tracks it or
|
|
1832
|
+
the `.gitignore` is gone; and `.agenthub/archive` is a
|
|
1833
|
+
`DENY_SEGMENTS` entry (`src/local/deny.ts`), so the local worker's and Pi's
|
|
1834
|
+
path guard, seatbelt profile and memory capture refuse it like
|
|
1835
|
+
`.agenthub/state`. A new 0700 state directory (created with `recursive`, as
|
|
1836
|
+
the status line tee may have re-created it) holds only a copy of
|
|
1837
|
+
`project.json`; the project id is the hash of its root,
|
|
1838
|
+
so the registration and id are unchanged. Archives are never pruned.
|
|
1839
|
+
- Output says that claude-mem is not touched and that attached Claude Code
|
|
1840
|
+
sessions must be relaunched with `ahub claude`. The control protocol is
|
|
1841
|
+
unchanged: reset never talks to a running hub except to inspect and stop it.
|
|
1842
|
+
|
|
1843
|
+
## Amendment: model-based PII screen (issue #198)
|
|
1844
|
+
|
|
1845
|
+
- `routing.toml` `[signals] pii_screen = "off" | "local"`, default `off`; any other
|
|
1846
|
+
value fails the load like a bad pattern. With `off`, or with `constraints.pii =
|
|
1847
|
+
"off"`, nothing changes and no screen call is made.
|
|
1848
|
+
- Order. With `local`, `Tasks.propose` screens the task's title, detail and plan
|
|
1849
|
+
text right after the pattern check and before triage. "Held as private" is met
|
|
1850
|
+
by holding the task back entirely: until the verdict it is not on the board, so
|
|
1851
|
+
no envelope, notice, triage call, brief or note of the hub's can read it. The
|
|
1852
|
+
local worker's claude-mem capture of its own tool calls is outside this: in an
|
|
1853
|
+
ordinary turn it sends a proposal's arguments whatever the verdict (an existing
|
|
1854
|
+
gap for every hub tool, not closed here). Writing it as
|
|
1855
|
+
a PII task first was rejected: a PII task opening lifts every silent turn-free
|
|
1856
|
+
cohort for good and switches facts off (#107, #108), so every proposal would do
|
|
1857
|
+
that. The proposal's tool call waits for the verdict, as it already waits for
|
|
1858
|
+
triage: for an ordinary task at worst the screen (8 s), triage (8 s) and a brief
|
|
1859
|
+
(2 x 2 s), 20 s; for a task the screen marked PII, triage first runs its own
|
|
1860
|
+
on-campus probe (two 4 s rounds when the gateway stalls), so about 24 s. Both
|
|
1861
|
+
stay under the plugin's 30 s control timeout. `hub_task_done` and `hub_review`
|
|
1862
|
+
put the screen (8 s) ahead of their existing waits, such as the briefs
|
|
1863
|
+
`releaseDependents` fetches for the tasks an approval releases.
|
|
1864
|
+
- The screener (`screenPii`, `src/hub/inference.ts`): the on-device model when
|
|
1865
|
+
`mlx.enabled` and it can be had (`ensureMlx`, loopback only; a legacy MLX runtime
|
|
1866
|
+
is started the way Pi's relay starts it and keeps running until `ahub models
|
|
1867
|
+
stop`), else the gateway's `local.fixed_model` only while the
|
|
1868
|
+
daemon's `onCampus()` holds, with `onCampusOnly` so the client refuses an Access
|
|
1869
|
+
host once more before transport; never the Switchyard sidecar. One call per item,
|
|
1870
|
+
an 8 s deadline over the whole call, and a generation slot of its own: it asks
|
|
1871
|
+
for one without waiting (`acquire(abort.signal, 0)`: one look, then
|
|
1872
|
+
`MlxBusyError`); with all taken (Pi generating, another screen) it uses the
|
|
1873
|
+
campus gateway when `onCampus()` holds and otherwise waits for the slot under the
|
|
1874
|
+
deadline. On campus the device gets 60% of the deadline: an error reply or a
|
|
1875
|
+
device still loading then hands over to the gateway within the rest; off campus
|
|
1876
|
+
it keeps the whole deadline. `max_tokens` 32, temperature 0 and `reasoning_effort: "none"` on both
|
|
1877
|
+
paths, so a reasoning model does not spend the budget thinking. The
|
|
1878
|
+
prompt frames the text as data and carries Korean and English examples and hard
|
|
1879
|
+
negatives. The answer must be exactly `clear` or `pii <category>` with a category
|
|
1880
|
+
from `name, student_id, phone, address, grade, health, other`.
|
|
1881
|
+
- Fail closed: an answer outside that form, a timeout, a failed or missing model,
|
|
1882
|
+
an off-campus gateway, or text over 6000 UTF-8 bytes (every token is at least a
|
|
1883
|
+
byte, so text and prompt fit the 8k on-device context; Ollama could otherwise
|
|
1884
|
+
judge cut input) is `unknown`. `pii` and `unknown` add the `pii` signal,
|
|
1885
|
+
so the task takes the whole existing PII path (local or nobody, user review,
|
|
1886
|
+
private envelopes, redacted views, no claude-mem). A pattern match is PII without
|
|
1887
|
+
a call. The source is recorded as a `screened` history entry (`pii: regex`,
|
|
1888
|
+
`pii: screen, <category>`, `pii: unknown, <reason>`, `clear`) and a `pii_screen`
|
|
1889
|
+
event; an unknown verdict is a console and hub.log line with the task id and the
|
|
1890
|
+
closed reason only. `route explain` on a draft does not call the screen and adds
|
|
1891
|
+
a trace line saying a proposal would. The cost of `unknown` is the cost of any PII
|
|
1892
|
+
task, even if a re-screen clears it later: it lifts every silent turn-free cohort
|
|
1893
|
+
for good and turns facts off while open, and off campus triage is not asked, so a
|
|
1894
|
+
classless proposal that is not a claim is refused with an error naming that.
|
|
1895
|
+
- Re-screen. Off campus with the device slot taken, the verdict is `unknown`. A
|
|
1896
|
+
task whose last `screened` entry is `pii: unknown` (not `too long`, which a
|
|
1897
|
+
second look cannot change), that is still `proposed` and that no peer ever owned
|
|
1898
|
+
(no history entry carries an owner) has had nothing private sent anywhere, so
|
|
1899
|
+
`Tasks.rescreen()`, on the daemon's 60 s release timer (one task per tick, the
|
|
1900
|
+
least tried first, at most 10 tries per task in a hub run, in memory), screens it
|
|
1901
|
+
again. `clear` lifts the `pii` signal through `Board.update` (a `screened` entry
|
|
1902
|
+
`clear, screened again`), notifies by task, and routes it through the class
|
|
1903
|
+
peers (a named owner or a claim is not remembered; a reserved owner, #207, is
|
|
1904
|
+
offered first as `assign()` always does); `pii` settles it; another `unknown`
|
|
1905
|
+
changes only the event record. A task an owner took (`local`) keeps its PII path,
|
|
1906
|
+
also after it was declined back to nobody: its review notes, summary and decline
|
|
1907
|
+
reason were never screened.
|
|
1908
|
+
- Free text (#69): the done summary, the review note with its unmet items, a
|
|
1909
|
+
budget hand-off (once per hand-off, however many tasks it moves) and a
|
|
1910
|
+
`hub_remember` note go through the same screen on an ordinary task when they
|
|
1911
|
+
match no pattern; a match is recorded as a `regex` verdict without a call. The
|
|
1912
|
+
call's cheap checks (owner, state, verdict, a running check, memory enabled) run
|
|
1913
|
+
first, then the screen, then the checks again on a fresh read. An escalation's
|
|
1914
|
+
reason is the hub's own text and stays pattern-only. `pii` or `unknown` withholds the
|
|
1915
|
+
item exactly as a pattern match does (stubs, no memory note, no summary line in
|
|
1916
|
+
completed-change or integration notices; `hub_remember` is refused). The board
|
|
1917
|
+
keeps the text and marks the history entry `withheld: true`, so public views,
|
|
1918
|
+
later reviewers, held cohort notices and `ahub ask` keep withholding it after a
|
|
1919
|
+
restart without another call. The check output tail of a done note, a plan given
|
|
1920
|
+
at accept, a decline reason, refs (paths, branch) and context checkpoint
|
|
1921
|
+
summaries stay pattern-only.
|
|
1922
|
+
- Calibration (AC4): `test/fixtures/pii-screen.json` holds synthetic Korean and
|
|
1923
|
+
English positives across the categories and hard negatives (roles,
|
|
1924
|
+
placeholders, field and function names, ports, codes); none is copied from the
|
|
1925
|
+
prompt, and its account, passport and resident numbers have impossible lengths
|
|
1926
|
+
or dates. `bun scripts/pii-screen-eval.ts [--project <dir>] [--out <file>]` screens
|
|
1927
|
+
each item once, in order, through `screenPii` with the project's configuration
|
|
1928
|
+
and reports recall, recall as handled (pii or unknown), precision, the unknown
|
|
1929
|
+
rate, held negatives, category agreement and latency. The bound, fixed here
|
|
1930
|
+
before any measurement: the model itself labels at least 90% of the positives
|
|
1931
|
+
`pii` (an unknown verdict counts as a miss). The report goes to
|
|
1932
|
+
`docs/verification/`; the script exits 1 under the bound.
|
|
@@ -18,6 +18,8 @@ Issue: #25. Pi is a managed project peer for implementation, edits, tests, summa
|
|
|
18
18
|
- Apple Silicon setup pins Python 3.12, `mlx-lm==0.31.3`, and `Qwen/Qwen3-8B-MLX-4bit` revision `383413e909f3bc5303ce195ebbdf0339c5a1a2a3`.
|
|
19
19
|
- The MLX input estimate is capped at 16,000 tokens; decode and prompt concurrency are one. DGX's default input estimate cap is 262,144 tokens.
|
|
20
20
|
- The relay authenticates every call, rejects browser origins and unknown model aliases, and preserves streamed bytes. It records the requested route and model reported by the backend. Per request it also journals sanitized identity and lifecycle evidence (#139): resolved alias, upstream-configured model, observed provider and served model with their source (gateway header, generation SSE event, or local MLX configuration), outcome (completed, cancelled, failed), duration, and a confirmed-mismatch flag. A request cancelled before any identification stays explicitly unidentified; a backend's mutable last-served label is never a request's own evidence, and a primary/auxiliary role stays unknown without native evidence. Records carry no prompts, tools, keys or Access headers.
|
|
21
|
+
- Load and cooldowns (#199, amended 2026-10-09). A `hub/auto` request bound for MLX keeps exactly its two candidates, MLX then its DGX fallback; load and cooldowns only reorder them, and a fixed alias (`mlx/fast`, `pi_backend = "mlx"`) is never reordered. The fallback goes first when MLX is cooling down, unless the fallback is cooling down or its own last dispatch failed in any way (an error status or a failed stream included) within 30 s and it has not succeeded since. Load moves are opt-in: with `[pi] efficient_wait_ms` set (below the 120 s slot wait; read when the hub first starts Pi), a movable request whose MLX slot is still busy after it, and an enforced tool loop pinned to the fallback, put the fallback first under the same health rule, and only while the request runs under no execution budget, where a move could spend the model call or the time MLX needs (budget-aware moves from the admission's remaining units are the upgrade path). Under #197 `enforce` a load move happens only at a user turn. A moved attempt gets a 15 s deadline to its response headers, after the gateway lookup, and is then abandoned for MLX; a reorder for a cooling MLX keeps the full request deadline. The bound ends with the headers: a stream that stalls or fails after them cannot fall back to MLX, and a failed one marks the fallback failing so later requests do not move. Each relay alias cools down after three consecutive transport or startup failures outside a cooldown (a failure more than twice the cap after the last counted one starts the count over), for 30 s doubling up to 5 min; a busy MLX slot and timeouts cut short by the execution budget never count, and any HTTP answer, a success or an error status, proves the transport works and clears the streak and the cooldown. A cooling DGX alias with no alternative is still tried. An alias that fails before it has a status row (gateway or key unavailable) gets one, so its cooldown shows. Both reorders are invisible to PII, which never reaches the relay (Pi refuses PII work). A load move journals no MLX attempt unless MLX then serves as the fallback. Route events record `source: "load"` or `"cooldown"` and `stateless` for a request without a session key; `events.jsonl` records `cooldown` start and end, and `ahub status` shows the cooldown on the backend line. Before the relay first starts MLX, startup decides as before. The live wait-time measurement on a mixed workload (#199 AC5) is open.
|
|
22
|
+
- Decision, 2026-10-09: load moves are off unless `efficient_wait_ms` is set, a deviation from the issue's "default about 500 ms". Reason: the move is unmeasured (AC5 is open), its bound covers only the response headers, and each review round found another way an unmeasured move could make a request worse than main. Revisit with the AC5 measurement.
|
|
21
23
|
- MLX may fall back to DGX before response streaming begins. Failed non-PII tasks can escalate to configured cloud peers with an explicit warning to reconcile partial effects.
|
|
22
24
|
- Runtime ownership includes process start identity. Project shutdown releases its relay, while explicit `ahub models stop` controls the shared MLX process.
|
|
23
25
|
|