agentus 0.3.1 → 0.3.3

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/README.md CHANGED
@@ -24,7 +24,7 @@ agents: open several sessions in the browser, each one backed by a real
24
24
  ```bash
25
25
  # public one-liner — the published package (Node >= 22.5; the store is node:sqlite)
26
26
  npx agentus # …or: npm i -g agentus, then `agentus`
27
- # open http://localhost:8787 and log in (admin / 123456 — see "Login" below),
27
+ # open http://localhost:8788 and log in (admin / 123456 — see "Login" below),
28
28
  # then "+ new session", pick a backend + a working directory
29
29
  ```
30
30
 
@@ -102,7 +102,7 @@ layer never changes.
102
102
 
103
103
  | var | default | meaning |
104
104
  |---|---|---|
105
- | `AGENTUS_PORT` | `8787` | server port (never reads bare `PORT` — that name is polluted on shared hosts) |
105
+ | `AGENTUS_PORT` | `8788` | server port (never reads bare `PORT` — that name is polluted on shared hosts) |
106
106
  | `AGENTUS_DATA` | `~/.agentus` | where state lives; see "Where your state lives" for the full order (`<repo>/packages/server/.data` on a checkout that already has one) |
107
107
  | `AGENTUS_HERMES_CMD` | `hermes` | binary to spawn for the hermes backend |
108
108
  | `AGENTUS_QODER_CMD` | `~/.local/bin/qodercli` | ditto for qoder |
@@ -196,7 +196,7 @@ Auth environment:
196
196
  ### Exposing it publicly (tunnel / reverse proxy)
197
197
 
198
198
  Put `AGENTUS_BASIC_AUTH=:pass` in the server's environment, then point the tunnel at
199
- `<lan-ip>:8787`. That gives you two independent locks — Basic at the edge of the app, the
199
+ `<lan-ip>:8788`. That gives you two independent locks — Basic at the edge of the app, the
200
200
  operator login inside it — and scripts can still get in (`curl -u :pass` plus the machine
201
201
  token).
202
202
 
@@ -231,7 +231,7 @@ Two things to know about a tunnel in front of this app:
231
231
  #### TLS: a second listener, on purpose
232
232
 
233
233
  A plain TCP tunnel (SakuraFrp, safe-nat, `ssh -L`, most frp setups) forwards bytes — it
234
- does **not** terminate TLS for you. Point one at `:8787` and the operator's password and
234
+ does **not** terminate TLS for you. Point one at `:8788` and the operator's password and
235
235
  session cookie cross the internet in clear text. So the server can speak TLS itself:
236
236
 
237
237
  ```bash
@@ -242,7 +242,7 @@ scripts/make-cert.sh # self-signed, SAN = the name you actual
242
242
 
243
243
  | port | speaks | for |
244
244
  |---|---|---|
245
- | `AGENTUS_PORT` (8787) | plain HTTP | LAN, loopback, `curl`, scripts — no cert warning, CI unchanged |
245
+ | `AGENTUS_PORT` (8788) | plain HTTP | LAN, loopback, `curl`, scripts — no cert warning, CI unchanged |
246
246
  | `AGENTUS_TLS_PORT` (8443) | HTTPS (self-signed) | **the tunnel** — encrypts the public hop |
247
247
 
248
248
  Same handler, same routes, same auth; only the socket differs. Two listeners beat both
package/README.zh-CN.md CHANGED
@@ -22,7 +22,7 @@
22
22
  ```bash
23
23
  # 对外一条命令 —— 用 npm 上发布的包(Node >= 22.5,存储用 node:sqlite)
24
24
  npx agentus # 或者 npm i -g agentus 然后直接跑 `agentus`
25
- # 打开 http://localhost:8787,用 admin / 123456 登录(见下面「登录」),
25
+ # 打开 http://localhost:8788,用 admin / 123456 登录(见下面「登录」),
26
26
  # 然后「+ 新建会话」,选一个后端和一个工作目录
27
27
  ```
28
28
 
@@ -93,7 +93,7 @@ npm run agentus # the same CLI as `npx agentus`, straight from the
93
93
 
94
94
  | 变量 | 默认值 | 含义 |
95
95
  |---|---|---|
96
- | `AGENTUS_PORT` | `8787` | 服务端口(从不读取裸 `PORT` —— 这个名字在共享主机上被污染) |
96
+ | `AGENTUS_PORT` | `8788` | 服务端口(从不读取裸 `PORT` —— 这个名字在共享主机上被污染) |
97
97
  | `AGENTUS_DATA` | `~/.agentus` | 状态所在;完整顺序见「状态放在哪」(已有数据的老 checkout 上是 `<repo>/packages/server/.data`) |
98
98
  | `AGENTUS_HERMES_CMD` | `hermes` | hermes 后端要拉起的二进制 |
99
99
  | `AGENTUS_QODER_CMD` | `~/.local/bin/qodercli` | qoder 同上 |
@@ -177,7 +177,7 @@ cookie 用 `HttpOnly` 而不是 `localStorage` 里的 token,并且它会顺带
177
177
 
178
178
  ### 公网暴露(隧道 / 反向代理)
179
179
 
180
- 把 `AGENTUS_BASIC_AUTH=:pass` 放进服务端环境,然后把隧道指向 `<lan-ip>:8787`。这样你就有了两把独立
180
+ 把 `AGENTUS_BASIC_AUTH=:pass` 放进服务端环境,然后把隧道指向 `<lan-ip>:8788`。这样你就有了两把独立
181
181
  的锁 —— 边缘的 Basic,里面的运维者登录 —— 脚本依然能进(`curl -u :pass` 加上机器 token)。
182
182
 
183
183
  这里推荐只用密码的形式(`:pass`,或裸 `pass`):HTTP Basic 总是会让浏览器问用户名(RFC 7617 传
@@ -205,7 +205,7 @@ cookie 用 `HttpOnly` 而不是 `localStorage` 里的 token,并且它会顺带
205
205
  #### TLS:刻意做成第二个监听器
206
206
 
207
207
  纯 TCP 隧道(SakuraFrp、safe-nat、`ssh -L`、大多数 frp 配置)只转发字节 —— 它**不会**替你终结 TLS。
208
- 把它指向 `:8787`,运维者的密码和 session cookie 就会以明文穿过互联网。所以服务端可以自己讲 TLS:
208
+ 把它指向 `:8788`,运维者的密码和 session cookie 就会以明文穿过互联网。所以服务端可以自己讲 TLS:
209
209
 
210
210
  ```bash
211
211
  scripts/make-cert.sh # self-signed, SAN = the name you actually type
@@ -215,7 +215,7 @@ scripts/make-cert.sh # self-signed, SAN = the name you actual
215
215
 
216
216
  | 端口 | 讲什么 | 给谁 |
217
217
  |---|---|---|
218
- | `AGENTUS_PORT` (8787) | 纯 HTTP | LAN、loopback、`curl`、脚本 —— 无证书警告,CI 不变 |
218
+ | `AGENTUS_PORT` (8788) | 纯 HTTP | LAN、loopback、`curl`、脚本 —— 无证书警告,CI 不变 |
219
219
  | `AGENTUS_TLS_PORT` (8443) | HTTPS(自签名) | **隧道** —— 加密公网那一跳 |
220
220
 
221
221
  同一套 handler,同一套路由,同一套认证;只有 socket 不同。两个监听器胜过另外两种做法:把单端口变成
package/bin/agentus.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * `agentus` — the command an npm install gives you.
4
4
  *
5
- * npx agentus # start the cockpit on :8787
5
+ * npx agentus # start the cockpit on :8788
6
6
  * npx agentus --port 9000 # …somewhere else
7
7
  * npx agentus --data ~/work # …with its state in a directory you chose
8
8
  * npx agentus --where # print the resolved paths and exit (nothing starts)
@@ -36,7 +36,7 @@ Usage
36
36
  agentus [options]
37
37
 
38
38
  Options
39
- -p, --port <n> port to listen on (AGENTUS_PORT, default 8787)
39
+ -p, --port <n> port to listen on (AGENTUS_PORT, default 8788)
40
40
  -d, --data <dir> where state lives — sqlite, login, TLS material, settings
41
41
  (AGENTUS_DATA, default ~/.agentus)
42
42
  -o, --open open the cockpit in your browser once it is up
@@ -115,7 +115,7 @@ if (opts.port !== undefined) env.AGENTUS_PORT = String(opts.port);
115
115
  if (opts.data !== undefined) env.AGENTUS_DATA = opts.data;
116
116
  if (opts.where) env.AGENTUS_PRINT_PATHS = "1";
117
117
 
118
- const port = env.AGENTUS_PORT ?? "8787";
118
+ const port = env.AGENTUS_PORT ?? "8788";
119
119
  if (!opts.where) {
120
120
  console.log(`[agentus] starting — open http://localhost:${port} when it is up (Ctrl-C stops it)`);
121
121
  }
@@ -11,7 +11,7 @@ instances:
11
11
  | | live | dev |
12
12
  |---|---|---|
13
13
  | path | `worktrees/agentus` | `worktrees/agentus-dev` |
14
- | HTTP port | `8787` | `8901` |
14
+ | HTTP port | `8788` | `8901` |
15
15
  | TLS port | `8443` | disabled (`AGENTUS_TLS_PORT=0`) |
16
16
  | data dir | `<tree>/packages/server/.data` | `/tmp/agentus-qa-account` |
17
17
  | started by | `~/.hermes/cache/agentus/launch.py` | `bash scripts/dev.sh start` |
@@ -85,7 +85,7 @@ which one won and why** — a directory change is the thing that would otherwise
85
85
  For the worktrees, the outcome is fixed by the launchers rather than by the rules:
86
86
 
87
87
  - **live** — `~/.hermes/cache/agentus/launch.py` pins `AGENTUS_DATA=<live>/packages/server/.data`
88
- and `AGENTUS_PORT=8787`, and pops the inherited QA/DEV keys. The launcher owns its
88
+ and `AGENTUS_PORT=8788`, and pops the inherited QA/DEV keys. The launcher owns its
89
89
  identity; it never trusts `os.environ`.
90
90
  - **dev** — `scripts/dev.sh` exports `AGENTUS_DATA=/tmp/agentus-qa-account` and
91
91
  `AGENTUS_PORT=8901`.
@@ -75,7 +75,7 @@ If `<slug>` is omitted, the branch name is used with `/` → `-`. What it does:
75
75
  | start shape | `scripts/start.sh`, `package.json` |
76
76
  | stale launcher | the live process is still running under `tsx watch` |
77
77
 
78
- 5. If a restart is needed, calls `scripts/relaunch.sh --port 8787 --tree <live>
78
+ 5. If a restart is needed, calls `scripts/relaunch.sh --port 8788 --tree <live>
79
79
  --launcher <live launcher>`, which TERMs the process group, waits for the port to free,
80
80
  starts the launcher, polls `/healthz`, and **asserts the pid changed** (a failure exits
81
81
  non-zero, so "built fine, still serving old code" cannot pass).
@@ -92,7 +92,7 @@ are all required:
92
92
  - `no server-side change` printed,
93
93
  - the **asset hash moved** (`web asset: <before> -> <after>`; `promote` warns if web sources
94
94
  changed but the hash did not),
95
- - the **pid did NOT move** (`lsof -ti:8787` unchanged — a moved pid means it took the
95
+ - the **pid did NOT move** (`lsof -ti:8788` unchanged — a moved pid means it took the
96
96
  restart branch and the release was not the kind it claimed to be).
97
97
 
98
98
  `package.json` is a restart trigger, so **any** edit to it — even one new npm alias for a QA
@@ -145,15 +145,15 @@ curl -s --noproxy '*' -H "Authorization: Bearer $TOK" http://127.0.0.1:8901/api/
145
145
  hash. `promote` echoes it; you can also read it directly:
146
146
 
147
147
  ```bash
148
- curl -s --noproxy '*' http://127.0.0.1:8787/api/version
148
+ curl -s --noproxy '*' http://127.0.0.1:8788/api/version
149
149
  ```
150
150
 
151
151
  **3. Grep the served bundle for the new markers.** Fetch the real vite-named file
152
152
  (`assets/index-<hash>.js`) and look for a string only the new code has:
153
153
 
154
154
  ```bash
155
- HASH=$(curl -s --noproxy '*' http://127.0.0.1:8787/api/version | ...)
156
- curl -s --noproxy '*' "http://127.0.0.1:8787/assets/$HASH.js" | grep -c '<new-marker>'
155
+ HASH=$(curl -s --noproxy '*' http://127.0.0.1:8788/api/version | ...)
156
+ curl -s --noproxy '*' "http://127.0.0.1:8788/assets/$HASH.js" | grep -c '<new-marker>'
157
157
  ```
158
158
 
159
159
  Two traps: the file is `index-<hash>.js`, so a check that fetches `/assets/<hash>.js`
@@ -195,6 +195,19 @@ git branch --set-upstream-to=origin/main main
195
195
  Judge the outcome by identity, not by "the push succeeded": `git ls-remote origin
196
196
  refs/heads/main` must equal the commit live is actually serving.
197
197
 
198
+ > **Do not merge into the live tree before promoting.** `promote` decides whether to restart by
199
+ > diffing **the live tree's old tip** against the commit it is promoting. If the live tree already
200
+ > sits on that commit (e.g. someone ran `git merge --ff-only <dev-branch>` inside
201
+ > `worktrees/agentus`), the diff is empty: promote prints `no server-side change`, ships the new web
202
+ > bundle, and **leaves the old server process running** — a half release that looks green. Restart
203
+ > detection is only as good as the tree it reads, so move `main` through the ref (`git push
204
+ > origin <dev-branch>:main`), never by checking it out in the live worktree.
205
+ >
206
+ > If it already happened: the release is not done. Re-run the restart explicitly
207
+ > (`scripts/relaunch.sh --port <live> --tree <live tree> --launcher <launcher> --launchd <label>`)
208
+ > and then resume the slots promote captured, exactly as promote's step 6 would have. Prove it by
209
+ > the startup log line for the new feature, not by the pid alone.
210
+
198
211
  ## Rollback
199
212
 
200
213
  The release path has no separate rollback mechanism — you revert the change and promote
@@ -23,13 +23,13 @@ empty. Triage the identity, not the liveness (see "Is it serving the wrong store
23
23
 
24
24
  | port | who |
25
25
  |---|---|
26
- | `8787` | live HTTP |
26
+ | `8788` | live HTTP |
27
27
  | `8443` | live TLS (public tunnel points here) |
28
28
  | `8901` | dev / QA instance |
29
29
  | `8648` | **Hermes Studio** — not Agentus, do not touch |
30
30
 
31
31
  ```bash
32
- lsof -nP -iTCP:8787 -sTCP:LISTEN # who owns the port (returns the CHILD, not the supervisor)
32
+ lsof -nP -iTCP:8788 -sTCP:LISTEN # who owns the port (returns the CHILD, not the supervisor)
33
33
  ps -o pid,ppid,pgid,command -p <pid> # its process group and command line
34
34
  ```
35
35
 
@@ -82,7 +82,7 @@ file at that same path, and its server was the only way into the machine. Rules:
82
82
  `spawn(..., { detached: true })` and kill the process group — rather than cleaning up by
83
83
  name afterwards.
84
84
  5. **Re-check the neighbours afterwards:** one `curl` per resident service
85
- (`:8648` Studio, `:8787`/`:8443` live, `:8901` dev). A cleanup that leaves the box worse
85
+ (`:8648` Studio, `:8788`/`:8443` live, `:8901` dev). A cleanup that leaves the box worse
86
86
  than it found it is the whole failure mode.
87
87
 
88
88
  ## The SQLite store, WAL, and the `*.bak` files
@@ -127,7 +127,7 @@ running slot was reaped — yet the port answers 200 and `/api/version` looks fi
127
127
  `AGENTUS_DATA`/`AGENTUS_PORT` and pops the QA/DEV keys, but verify identity directly:
128
128
 
129
129
  ```bash
130
- PID=$(lsof -nP -iTCP:8787 -sTCP:LISTEN -t)
130
+ PID=$(lsof -nP -iTCP:8788 -sTCP:LISTEN -t)
131
131
  ps eww -p "$PID" | tr ' ' '\n' | grep AGENTUS # the process's own identity vars
132
132
  lsof -p "$PID" | grep sqlite # which store it actually opened
133
133
  ```
@@ -137,11 +137,17 @@ Recovery:
137
137
  ```bash
138
138
  cd worktrees/agentus
139
139
  env -u AGENTUS_DATA -u AGENTUS_BASE -u DEV_LOG_PATH -u DEV_PROMOTE_DETACHED -u DEV_PROMOTE_SELF \
140
- bash scripts/relaunch.sh --port 8787 --launcher ~/.hermes/cache/agentus/launch.py
140
+ bash scripts/relaunch.sh --port 8788 --launcher ~/.hermes/cache/agentus/launch.py \
141
+ --launchd ai.hermes.agentus
141
142
  TOK=$(cat packages/server/.data/auth.token)
142
- curl -s -X POST -H "Authorization: Bearer $TOK" http://127.0.0.1:8787/api/sessions/<id>/resume
143
+ curl -s -X POST -H "Authorization: Bearer $TOK" http://127.0.0.1:8788/api/sessions/<id>/resume
143
144
  ```
144
145
 
146
+ `--launchd <label>` makes relaunch.sh hand the restart to launchd (`kickstart -k`) rather than
147
+ killing the process by hand — once the job is installed, a hand-kill is not a restart, it is a
148
+ respawn race (launchd brings the old process back while our own launcher starts a second one). Pass
149
+ it whenever the label is loaded; relaunch.sh ignores it when it is not.
150
+
145
151
  After any restart, put the operator back on the air: `POST /api/sessions/<id>/resume` with the
146
152
  machine token → `status: ready` and a fresh pid. **Capture the slot ids to resume before the
147
153
  restart** — in the window right after a restart a slot has dropped from `live` into
@@ -155,8 +161,8 @@ one-click problem. Endpoints behind the login:
155
161
 
156
162
  ```bash
157
163
  TOK=$(cat <DATA_DIR>/auth.token)
158
- curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:8787/api/auth/locked-ips
159
- curl -s -X DELETE -H "Authorization: Bearer $TOK" http://127.0.0.1:8787/api/auth/locked-ips
164
+ curl -s -H "Authorization: Bearer $TOK" http://127.0.0.1:8788/api/auth/locked-ips
165
+ curl -s -X DELETE -H "Authorization: Bearer $TOK" http://127.0.0.1:8788/api/auth/locked-ips
160
166
  ```
161
167
 
162
168
  In the UI it is Settings → 登录失败锁定 (which IPs are locked, for how long, unlock / unlock
@@ -20,12 +20,35 @@ variables, and they must never touch each other's code or data:
20
20
  |---|---|---|
21
21
  | tree | `worktrees/agentus` | `worktrees/agentus-dev` |
22
22
  | role | the instance the operator actually uses | where changes are made and tested |
23
- | HTTP port | `8787` | `8901` |
23
+ | HTTP port | `8788` | `8901` |
24
24
  | TLS port | `8443` | off (`AGENTUS_TLS_PORT=0`) |
25
25
  | data dir | `<tree>/packages/server/.data` (pinned by the launcher) | `/tmp/agentus-qa-account` |
26
26
  | launcher | `~/.hermes/cache/agentus/launch.py` | `scripts/dev.sh start\|stop\|status\|logs` |
27
+ | supervision | launchd job `ai.hermes.agentus` (`KeepAlive`) | none — a plain detached process |
27
28
  | hot reload | **no** — restart is explicit | yes (`tsx watch`) |
28
29
 
30
+ **Ports are configuration, not literals.** Both of live's ports live in
31
+ `~/.hermes/cache/agentus/ports.env`; `launch.py` pins them into the server's environment, and
32
+ `scripts/dev.sh` / `scripts/relaunch.sh` read the same file instead of hardcoding a number. Move
33
+ live by editing that one line and restarting it — never by hunting a literal through the launchers,
34
+ the scripts and the docs (that is how the port and the TLS port both drifted before).
35
+
36
+ **Live is supervised by launchd.** It used to be a detached child of whatever shell happened to
37
+ start it: nothing brought it back after a crash, and it inherited that shell's entire environment
38
+ (usually an agent session's, handing `HERMES_SESSION_ID` and `WEIXIN_TOKEN` down to every ACP child
39
+ it spawned). Now:
40
+
41
+ ```bash
42
+ launchctl print gui/$(id -u)/ai.hermes.agentus # state, pid, the job's environment
43
+ launchctl kickstart -k gui/$(id -u)/ai.hermes.agentus # the supported restart (stop then start)
44
+ tail -f /tmp/agentus-server.log # the job's stdout/stderr
45
+ ```
46
+
47
+ `scripts/relaunch.sh --launchd ai.hermes.agentus` is the same thing wrapped with a health check and
48
+ a pid-moved assertion, and that is what `promote` uses. Launchd's `KeepAlive` restarts it on any
49
+ exit, so a crash self-heals; a **manual** `kill` therefore no longer restarts live, it just makes
50
+ launchd respawn it.
51
+
29
52
  **Why two trees.** `tsx watch` reloads the whole server on any server-source edit, and a
30
53
  reload SIGTERMs every child process — every live `hermes acp` session, including the one
31
54
  the operator is typing into. Editing server code in the live tree would therefore kill his
@@ -91,7 +91,7 @@ serves once (no watcher). `AGENTUS_PORT=9000 npm start` moves the port.
91
91
  ```
92
92
  agentus [options]
93
93
 
94
- -p, --port <n> port to listen on (AGENTUS_PORT, default 8787)
94
+ -p, --port <n> port to listen on (AGENTUS_PORT, default 8788)
95
95
  -d, --data <dir> where state lives (sqlite, login, TLS material, settings)
96
96
  (AGENTUS_DATA, default ~/.agentus)
97
97
  -o, --open open the cockpit in your browser once it is up
@@ -107,7 +107,7 @@ answer to "where did my data go" (see below).
107
107
  ## First run
108
108
 
109
109
  1. Start it: `agentus` (or `npx agentus`, or `npm start` from a checkout).
110
- 2. Open **http://localhost:8787** and log in. Default credentials:
110
+ 2. Open **http://localhost:8788** and log in. Default credentials:
111
111
 
112
112
  ```
113
113
  username: admin
@@ -130,7 +130,7 @@ unless the backend row names another one — see [HERMES_HOME](#hermes_home).
130
130
 
131
131
  | var | default | meaning |
132
132
  |---|---|---|
133
- | `AGENTUS_PORT` | `8787` | server port (never reads bare `PORT`) |
133
+ | `AGENTUS_PORT` | `8788` | server port (never reads bare `PORT`) |
134
134
  | `AGENTUS_DATA` | `~/.agentus` | where state lives; see [Data directory](#data-directory) |
135
135
  | `AGENTUS_HERMES_CMD` | `hermes` | binary to spawn for the hermes backend |
136
136
  | `AGENTUS_QODER_CMD` | `~/.local/bin/qodercli` | ditto for qoder |
@@ -189,7 +189,7 @@ with it.
189
189
 
190
190
  | port | speaks | for |
191
191
  |---|---|---|
192
- | `AGENTUS_PORT` (8787) | plain HTTP | LAN, loopback, `curl`, scripts — no cert warning, CI unchanged |
192
+ | `AGENTUS_PORT` (8788) | plain HTTP | LAN, loopback, `curl`, scripts — no cert warning, CI unchanged |
193
193
  | `AGENTUS_TLS_PORT` (8443) | HTTPS (self-signed) | the tunnel — encrypts the public hop |
194
194
  | 5173 | Vite dev server | front-end hot reload, only during `npm run dev` |
195
195
 
@@ -268,7 +268,7 @@ a different session list.
268
268
  <key>Label</key> <string>org.agentus.cockpit</string>
269
269
  <key>ProgramArguments</key>
270
270
  <array>
271
- <string>/usr/bin/env</string><string>agentus</string><string>--port</string><string>8787</string>
271
+ <string>/usr/bin/env</string><string>agentus</string><string>--port</string><string>8788</string>
272
272
  </array>
273
273
  <key>EnvironmentVariables</key>
274
274
  <dict><key>AGENTUS_DATA</key><string>/Users/YOU/.agentus</string></dict>
@@ -293,7 +293,7 @@ Description=Agentus cockpit
293
293
  After=network.target
294
294
 
295
295
  [Service]
296
- ExecStart=/usr/bin/env agentus --port 8787
296
+ ExecStart=/usr/bin/env agentus --port 8788
297
297
  Environment=AGENTUS_DATA=%h/.agentus
298
298
  Restart=on-failure
299
299
 
@@ -335,7 +335,7 @@ touches `~/.hermes` (your agents' own data) or any working directory.
335
335
  ## Troubleshooting
336
336
 
337
337
  **Port already in use.** `agentus --port 9000`, or find and stop the holder:
338
- `lsof -nP -iTCP:8787 -sTCP:LISTEN`. Agentus never reads bare `PORT` for exactly this reason.
338
+ `lsof -nP -iTCP:8788 -sTCP:LISTEN`. Agentus never reads bare `PORT` for exactly this reason.
339
339
 
340
340
  **"No backend" / a session won't start.** The agent CLI is missing or not logged in. Run
341
341
  `hermes acp --check`, or `qodercli login` (a `-32000` on `newSession` means qoder is not logged
@@ -362,5 +362,5 @@ requests. The live stream rides on `/ws`; use a tunnel that passes WebSockets th
362
362
  TCP+auto-HTTPS tunnels are verified working).
363
363
 
364
364
  **Certificate warning on the LAN.** That is the TLS listener's self-signed cert (a bare LAN IP
365
- cannot carry a trusted cert). Either use the plain-HTTP `:8787` port on the LAN, or install the
365
+ cannot carry a trusted cert). Either use the plain-HTTP `:8788` port on the LAN, or install the
366
366
  issuer once from `/cert.crt` (see [TLS](#tls)).
@@ -88,7 +88,7 @@ npm run agentus # 与 `npx agentus` 同一个 CLI,直接
88
88
  ```
89
89
  agentus [options]
90
90
 
91
- -p, --port <n> 监听端口 (AGENTUS_PORT,默认 8787)
91
+ -p, --port <n> 监听端口 (AGENTUS_PORT,默认 8788)
92
92
  -d, --data <dir> 状态目录(sqlite、登录、TLS 材料、设置)
93
93
  (AGENTUS_DATA,默认 ~/.agentus)
94
94
  -o, --open 就绪后自动在浏览器打开驾驶舱
@@ -103,7 +103,7 @@ agentus [options]
103
103
  ## 首次运行
104
104
 
105
105
  1. 启动:`agentus`(或 `npx agentus`,或 checkout 里的 `npm start`)。
106
- 2. 打开 **http://localhost:8787** 登录。默认凭据:
106
+ 2. 打开 **http://localhost:8788** 登录。默认凭据:
107
107
 
108
108
  ```
109
109
  username: admin
@@ -125,7 +125,7 @@ agentus [options]
125
125
 
126
126
  | 变量 | 默认值 | 含义 |
127
127
  |---|---|---|
128
- | `AGENTUS_PORT` | `8787` | 服务端口(从不读裸 `PORT`) |
128
+ | `AGENTUS_PORT` | `8788` | 服务端口(从不读裸 `PORT`) |
129
129
  | `AGENTUS_DATA` | `~/.agentus` | 状态目录;见[数据目录](#数据目录) |
130
130
  | `AGENTUS_HERMES_CMD` | `hermes` | hermes 后端要拉起的可执行文件 |
131
131
  | `AGENTUS_QODER_CMD` | `~/.local/bin/qodercli` | qoder 后端同理 |
@@ -181,7 +181,7 @@ agentus [options]
181
181
 
182
182
  | 端口 | 协议 | 用途 |
183
183
  |---|---|---|
184
- | `AGENTUS_PORT`(8787) | 纯 HTTP | 局域网、本机回环、`curl`、脚本 —— 无证书警告,CI 不变 |
184
+ | `AGENTUS_PORT`(8788) | 纯 HTTP | 局域网、本机回环、`curl`、脚本 —— 无证书警告,CI 不变 |
185
185
  | `AGENTUS_TLS_PORT`(8443) | HTTPS(自签) | **隧道** —— 加密公网那一跳 |
186
186
  | 5173 | Vite 开发服务器 | 前端热更新,仅在 `npm run dev` 期间 |
187
187
 
@@ -257,7 +257,7 @@ Agentus 默认在前台跑;一个服务单元能让它跨登录、跨重启常
257
257
  <key>Label</key> <string>org.agentus.cockpit</string>
258
258
  <key>ProgramArguments</key>
259
259
  <array>
260
- <string>/usr/bin/env</string><string>agentus</string><string>--port</string><string>8787</string>
260
+ <string>/usr/bin/env</string><string>agentus</string><string>--port</string><string>8788</string>
261
261
  </array>
262
262
  <key>EnvironmentVariables</key>
263
263
  <dict><key>AGENTUS_DATA</key><string>/Users/YOU/.agentus</string></dict>
@@ -282,7 +282,7 @@ Description=Agentus cockpit
282
282
  After=network.target
283
283
 
284
284
  [Service]
285
- ExecStart=/usr/bin/env agentus --port 8787
285
+ ExecStart=/usr/bin/env agentus --port 8788
286
286
  Environment=AGENTUS_DATA=%h/.agentus
287
287
  Restart=on-failure
288
288
 
@@ -323,7 +323,7 @@ rm -rf ~/.agentus # 删状态 —— SQLite、登录、TLS 密
323
323
  ## 排错
324
324
 
325
325
  **端口被占用。** 用 `agentus --port 9000`,或找出并停掉占用者:
326
- `lsof -nP -iTCP:8787 -sTCP:LISTEN`。Agentus 从不读裸 `PORT`,原因正在于此。
326
+ `lsof -nP -iTCP:8788 -sTCP:LISTEN`。Agentus 从不读裸 `PORT`,原因正在于此。
327
327
 
328
328
  **「没有后端」/ 会话起不来。** 缺少 agent CLI,或它没登录。跑 `hermes acp --check`,或
329
329
  `qodercli login`(`newSession` 报 `-32000` 就是 qoder 没登录)。在设置里检查后端行的命令;启动
@@ -347,4 +347,4 @@ rm -rf ~/.agentus # 删状态 —— SQLite、登录、TLS 密
347
347
  `/ws`;请换成能透传 WebSocket 的隧道(SakuraFrp 的 TCP+auto-HTTPS 隧道已验证可用)。
348
348
 
349
349
  **局域网里出现证书警告。** 那是 TLS 监听的自签证书(裸局域网 IP 无法承载可信证书)。要么在
350
- 局域网里用纯 HTTP 的 `:8787` 端口,要么从 `/cert.crt` 装一次签发证书(见 [TLS](#tls))。
350
+ 局域网里用纯 HTTP 的 `:8788` 端口,要么从 `/cert.crt` 装一次签发证书(见 [TLS](#tls))。
@@ -131,7 +131,7 @@ And against real data — the one-off used when this landed, on the operator's o
131
131
  ```bash
132
132
  # rows → the CLIENT's fold, over the operator API
133
133
  curl -s -H "Authorization: Bearer $(cat "${AGENTUS_DATA:-$HOME/.agentus}/auth.token")" \
134
- 'http://127.0.0.1:8787/api/sessions/<id>/messages?tail=1'
134
+ 'http://127.0.0.1:8788/api/sessions/<id>/messages?tail=1'
135
135
  ```
136
136
 
137
137
  Result for `hello`: **3804 rows → 70** (10 agent messages, 15 thought blocks, 37 tool cards, 8 prompts),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentus",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Multi-session web cockpit for ACP-speaking coding agents (Qoder CLI / Hermes). UI has no intelligence — the agent lives in spawned CLI subprocesses.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -64,12 +64,12 @@
64
64
  "store-fold": "tsx scripts/qa/store-fold.mts",
65
65
  "live-stream": "tsx scripts/qa/live-stream.mts",
66
66
  "open-target": "tsx scripts/qa/open-target.mts",
67
- "rail-recent": "tsx scripts/qa/rail-recent.mts",
68
- "turn-pulse": "tsx scripts/qa/turn-pulse.mts",
69
- "resume-tail": "tsx scripts/qa/resume-tail.mts",
70
- "render-lag-sweep": "node scripts/qa/render-lag-sweep.mjs",
71
- "replay-window": "node scripts/qa/replay-window.mjs",
72
- "think-cadence": "node scripts/qa/think-cadence.mjs"
67
+ "rail-recent": "tsx scripts/qa/rail-recent.mts",
68
+ "turn-pulse": "tsx scripts/qa/turn-pulse.mts",
69
+ "resume-tail": "tsx scripts/qa/resume-tail.mts",
70
+ "render-lag-sweep": "node scripts/qa/render-lag-sweep.mjs",
71
+ "replay-window": "node scripts/qa/replay-window.mjs",
72
+ "think-cadence": "node scripts/qa/think-cadence.mjs"
73
73
  },
74
74
  "engines": {
75
75
  "node": ">=22.5"
@@ -7,6 +7,10 @@
7
7
  // MOCK_TOOL=1 -> prompt triggers a tool_call + requestPermission
8
8
  // MOCK_THINK=1 -> emits agent_thought_chunk before the answer
9
9
  // MOCK_SLOW_MS=n -> delay between chunks (default 60)
10
+ // MOCK_SPAWN_MS=n -> boot latency before the handshake answers (default 0). A real agent takes
11
+ // seconds to come up (node + initialize + session/load) and that window is
12
+ // where the cockpit's 「正在唤醒 agent 进程…」 state lives — a browser sweep
13
+ // cannot observe a state that lasts 80 ms.
10
14
  // MOCK_SINK=1 -> exit the process mid-turn (crash path for AC5/QA)
11
15
  import { randomUUID } from "node:crypto";
12
16
  import fs from "node:fs";
@@ -14,6 +18,10 @@ import { AgentSideConnection, RequestError, ndJsonStream } from "@agentclientpro
14
18
  import { Readable, Transform, Writable } from "node:stream";
15
19
 
16
20
  const SLOW = Number(process.env.MOCK_SLOW_MS || 60);
21
+ /** Boot latency — deliberately BEFORE the stream is wired, so the slot sits in `starting` for as
22
+ * long as QA asks. Defaults to 0: the mock is instant unless a sweep needs the wake window. */
23
+ const SPAWN_MS = Number(process.env.MOCK_SPAWN_MS || 0);
24
+ if (SPAWN_MS > 0) await new Promise((r) => setTimeout(r, SPAWN_MS));
17
25
 
18
26
  // The file a `[tool-diff]` turn pretends to edit: hermes hands the cockpit the whole file
19
27
  // before and after, so the approval surface has a path and a real change to show.