@rjfan/dsh-presence 0.19.0 → 0.20.1
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 +92 -23
- package/README.zh-CN.md +81 -20
- package/index.js +138 -89
- package/package.json +1 -1
- package/src/api.js +6 -1
- package/src/intercept.js +150 -0
- package/src/job-store.js +292 -0
- package/src/observed-store.js +136 -19
- package/src/pool.js +308 -86
- package/src/tools.js +845 -154
- package/src/wait.js +352 -0
package/README.md
CHANGED
|
@@ -22,6 +22,14 @@ forwards for coding agents — with **nothing installed on the remote host**.
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
> ⚠️ **A local session is not bound to a remote workspace.** Opening a remote workspace in
|
|
26
|
+
> Presence does not turn a local agent session into a remote one. When your session's
|
|
27
|
+
> working directory is outside the presence mounts root, `presence_exec` and every other
|
|
28
|
+
> `presence_*` tool require an explicit `host` (or `PRESENCE_HOST`); without one they refuse
|
|
29
|
+
> with the session's cwd instead of silently running on the last-activated machine. Sessions
|
|
30
|
+
> started inside a remote workspace resolve to that machine automatically. See
|
|
31
|
+
> [Known limits](#known-limits).
|
|
32
|
+
|
|
25
33
|
## Why
|
|
26
34
|
|
|
27
35
|
Running an agent on every remote machine means installing it N times, upgrading it N
|
|
@@ -44,6 +52,10 @@ connection per host**. The remote side is still plain `sshd`: SFTP, exec and a P
|
|
|
44
52
|
- **Native remote tools** — `rg`/`grep` runs on the target, commands run in the target shell; path, permission and encoding semantics are the remote ones.
|
|
45
53
|
- **Local-like edits** — atomic writes, mode preservation, ambiguity refusal, read-before-write guard, `presence_edit` with dry-run diffs.
|
|
46
54
|
- **Real workspaces** — a remote directory becomes a normal DSH workspace with a file tree and terminals that open **on the remote machine**.
|
|
55
|
+
- **Sessions are scoped (0.20 hotfix)** — a local session no longer silently borrows the last-activated remote workspace: `presence_*` needs an explicit `host` or `PRESENCE_HOST`, and the system prompt tells each session whether it is local or remote.
|
|
56
|
+
- **Conditions instead of sleeps** — `presence_wait` blocks on a file (`exists`/`minBytes`/`mtimeAfter`), a grep match count, a process (`pid`/`pattern`), a port state or a job id; a timeout is `met:false`, not an error.
|
|
57
|
+
- **Jobs that survive a restart** — background jobs are persisted to `~/.dsh/presence/jobs.json` with pid/pgid. `logPath` is the job log to tail; `commandLogPath` is the command's own log and equals `logPath` on current kernels (a separate wrapper log only exists when a kernel records one) — use `commandLogPath ?? logPath`. After a restart their state is `unknown` until probed — never a false `running`, and an exit code may be missing.
|
|
58
|
+
- **Batch transfer and append** — `presence_import`/`presence_export` take `paths[]` (1–50) or a glob and return per-file results; `presence_write { mode:"append" }` appends without rewriting the file.
|
|
47
59
|
- **Recoverable deletes** — destructive shell commands dry-run first; `presence_trash` moves paths to a remote trash with restore.
|
|
48
60
|
- **`@` completes remote files** — inside a remote workspace, file-reference completion indexes the machine itself (`rg --files`, `find` fallback), not a local mirror.
|
|
49
61
|
- **File operations, not just reads** — `presence_mkdir` / `move` / `copy` / `chmod` plus a right-click menu in the file tree; deletes go to the recoverable trash.
|
|
@@ -95,7 +107,7 @@ higher median interaction latency.</sub>
|
|
|
95
107
|
### 1. Install the DSH plugin
|
|
96
108
|
|
|
97
109
|
```sh
|
|
98
|
-
dsh plugin add @rjfan/dsh-presence@0.
|
|
110
|
+
dsh plugin add @rjfan/dsh-presence@0.20.1
|
|
99
111
|
```
|
|
100
112
|
|
|
101
113
|
From a source checkout:
|
|
@@ -110,6 +122,10 @@ Open **Settings → Presence → New**, choose a host from `~/.ssh/config` (or p
|
|
|
110
122
|
manual `ssh` command), then pick a remote folder. It appears as a workspace; the
|
|
111
123
|
left sidebar, file tree, terminal and agent tools all use that remote directory.
|
|
112
124
|
|
|
125
|
+
A session started in that workspace is remote. A session started anywhere else (for
|
|
126
|
+
example a normal local project) stays local: it must pass `host` explicitly — or set
|
|
127
|
+
`PRESENCE_HOST` — before calling `presence_exec` / `presence_*`.
|
|
128
|
+
|
|
113
129
|
### 3. Work
|
|
114
130
|
|
|
115
131
|
```text
|
|
@@ -184,25 +200,27 @@ Measured on a Linux host:
|
|
|
184
200
|
|
|
185
201
|
## Tools
|
|
186
202
|
|
|
187
|
-
The DSH plugin exposes
|
|
188
|
-
workspace
|
|
189
|
-
|
|
190
|
-
|
|
203
|
+
The DSH plugin exposes 25 tools. Inside a remote-workspace session relative paths
|
|
204
|
+
resolve against that workspace and `host` is optional; a **local** session must pass
|
|
205
|
+
`host` explicitly (or configure `PRESENCE_HOST`) — there is no fallback to the globally
|
|
206
|
+
last-activated workspace. Argument aliases are accepted at execute time
|
|
207
|
+
(`path`/`file_path`/`filePath`, `old`/`oldString`, `new`/`newString`, `cwd`/`workdir`, …),
|
|
208
|
+
so an agent's habitual spelling still works.
|
|
191
209
|
|
|
192
210
|
<details>
|
|
193
211
|
<summary><b>Show all tools</b></summary>
|
|
194
212
|
|
|
195
213
|
| Tool | What it does |
|
|
196
214
|
| --- | --- |
|
|
197
|
-
| `presence_hosts` | List SSH hosts, connection state and default host (`format:"json"` supported); `workspace:true` marks this session's machine and `degraded` rows carry the last error. |
|
|
215
|
+
| `presence_hosts` | List SSH hosts, connection state and default host (`format:"json"` supported); `workspace:true` marks this session's machine and `degraded` rows carry the last error. `default` is session-scoped: this session's mounted host or `PRESENCE_HOST`, never a global last-active workspace. |
|
|
198
216
|
| `presence_connect` | Open/reuse a host connection and report the remote identity. |
|
|
199
|
-
| `presence_exec` | Run a remote command; `pipefail`, `background`, `waitForPort`, structured JSON, `saveStdout`; timeout defaults to 300 s; `progress` normalizes `\r` progress bars; long output spills to a remote file whose path is returned (`tailLines`); destructive commands dry-run first — `confirm:true` runs the whole command, `confirm:"partial"` runs only the safe segments. |
|
|
200
|
-
| `presence_jobs` |
|
|
217
|
+
| `presence_exec` | Run a remote command; `pipefail`, `background`, `waitForPort`, structured JSON, `saveStdout`; timeout defaults to 300 s; `progress` normalizes `\r` progress bars; long output spills to a remote file whose path is returned (`tailLines`); text reports start with `host: <alias> remote cwd: <abs>`; destructive commands dry-run first — `confirm:true` runs the whole command, `confirm:"partial"` runs only the safe segments. A local session must pass `host`. |
|
|
218
|
+
| `presence_jobs` | Background jobs: `list`, `status`, `wait` (with `timedOut`), `stop` (with `alive`/`verified`/`portReleased`), `purge`. Jobs persist to `~/.dsh/presence/jobs.json` with `pid`/`pgid`; `logPath` is the job log to tail (the command's stdout/stderr on current kernels) and `commandLogPath` the command's own log — equal to `logPath` on current kernels, so use `commandLogPath ?? logPath`. After a restart, state is `unknown` until a liveness probe succeeds and the exit code may be missing. `/presence/api/state` job rows also carry `pid`, `pgid`, `commandLogPath` and `persisted`. |
|
|
219
|
+
| `presence_wait` | Block until exactly one remote condition is true: `file {path, exists?, minBytes?, mtimeAfter?}`, `grep {path, pattern, count?}`, `process {pid\|pattern, alive?}`, `port {port, state:"open"\|"closed"}` or `job {jobId}` (a job condition needs no `host` — the registry knows where it ran). Returns `{ met, timedOut, elapsedMs, observed, condition, polls }`; a timeout is `met:false` and **never an error**. `timeoutMs` default 120 s (cap 1 h), `pollMs`/`intervalMs` default 1000 ms. |
|
|
201
220
|
| `presence_read` | Read a remote file with line numbers, `offset`/`limit`, or `raw:true` for verbatim content. |
|
|
202
221
|
| `presence_read_many` | Read 1–50 remote files in one call, with per-file byte/line caps. |
|
|
203
|
-
| `presence_write` | Atomic write; preserves mode; returns real mode, size and full sha256; `ifMatchSha256` optimistic lock. |
|
|
222
|
+
| `presence_write` | Atomic write; preserves mode; returns real mode, size and full sha256; `ifMatchSha256` optimistic lock; `mode:"append"` appends at EOF and reports appended bytes + total size (`ifMatchSha256` is overwrite-only and append rejects it); `mode` also accepts an octal string such as `"755"` to set permissions on an overwrite. |
|
|
204
223
|
| `presence_edit` | Exact-text replacement or line-range replacement, with compact diff, `dryRun`, JSON fields. |
|
|
205
|
-
| `presence_ls` | List a directory; `recursive`, `totalBytes`, JSON output. |
|
|
206
224
|
| `presence_stat` | Batch `stat` for 1–100 paths: type, size, mode, mtime, optional sha256; missing paths return `exists:false`. |
|
|
207
225
|
| `presence_mkdir` | Create a remote directory (recursive by default); idempotent when it already exists. |
|
|
208
226
|
| `presence_move` | Rename or move a path inside the same machine; refuses to replace an existing file unless `overwrite:true`. |
|
|
@@ -212,8 +230,8 @@ execute time (`path`/`file_path`/`filePath`, `old`/`oldString`, `new`/`newString
|
|
|
212
230
|
| `presence_tail` | Read the last N lines of a remote file, or follow it for a bounded time (`follow:true`, `timeoutMs`). |
|
|
213
231
|
| `presence_search` | Remote `rg`/`grep` search, count-only mode, JSON matches. |
|
|
214
232
|
| `presence_glob` | Find remote files by glob with size/mtime. |
|
|
215
|
-
| `presence_import` | Local → remote binary-safe upload with mode and sha256. |
|
|
216
|
-
| `presence_export` | Remote → local real file (content-addressed), for preview or native apps. |
|
|
233
|
+
| `presence_import` | Local → remote binary-safe upload with mode and sha256; batch `paths[]` (1–50) and/or a local `glob` into a `to` directory, with one result per file. |
|
|
234
|
+
| `presence_export` | Remote → local real file (content-addressed), for preview or native apps; batch `paths[]` or a remote `glob`; the local name defaults to the basename and a collision becomes `name-<sha8>.ext`; one result per file. |
|
|
217
235
|
| `presence_forward` | Forward a remote port to `127.0.0.1`; open/list/close with JSON. |
|
|
218
236
|
| `presence_port_probe` | Check listeners and TCP reachability for up to 20 ports; `expect:open|closed`, polling. |
|
|
219
237
|
| `presence_http_probe` | Wait for port → forward → local HTTP assertions → close → verify the local port was released. |
|
|
@@ -222,14 +240,29 @@ execute time (`path`/`file_path`/`filePath`, `old`/`oldString`, `new`/`newString
|
|
|
222
240
|
|
|
223
241
|
</details>
|
|
224
242
|
|
|
243
|
+
### Batch transfer and append
|
|
244
|
+
|
|
245
|
+
```text
|
|
246
|
+
presence_export { paths: ["/data/a.pt", "/data/b.pt"] } # files:[{remotePath, localPath, bytes, sha256}]
|
|
247
|
+
presence_export { glob: "/data/ckpt-*.pt" } # every match; name-<sha8>.ext on collision
|
|
248
|
+
presence_import { glob: "dist/*.whl", to: "/data/wheels" } # local → remote, one result per file
|
|
249
|
+
presence_write { path: "/data/train.log", mode: "append", content: "epoch 6 done\n" }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Single-path calls keep the existing `path`/`to` behaviour, so none of this is required for
|
|
253
|
+
one file.
|
|
254
|
+
|
|
225
255
|
## Which tool for which path
|
|
226
256
|
|
|
227
257
|
| Where the path lives | Use | Why |
|
|
228
258
|
| --- | --- | --- |
|
|
229
259
|
| under the workspace mount (`~/.dsh/presence/mounts/...`) | built-in `read` / `write` / `edit` / `grep` / `glob` | they reach the remote machine; `grep`/`glob` are routed to it automatically |
|
|
230
|
-
| remote absolute path outside the workspace (`/data`, `/scratch`, GPFS) | `presence_read` / `write` / `edit` / `
|
|
260
|
+
| remote absolute path outside the workspace (`/data`, `/scratch`, GPFS) | `presence_read` / `write` / `edit` / `stat` / `search` / `glob` (list a directory with `presence_exec`) | the built-in tools only see the mount, not the rest of the machine |
|
|
231
261
|
| local file, for preview or Office | `presence_export` first, then `present` / native open | the mount path is virtual; only a real local file can be opened natively |
|
|
232
262
|
|
|
263
|
+
In a **local** session (cwd outside the mounts root) the built-in tools only touch local
|
|
264
|
+
files; every `presence_*` call needs `host` explicitly, or configure `PRESENCE_HOST` once.
|
|
265
|
+
|
|
233
266
|
## Long-running tasks
|
|
234
267
|
|
|
235
268
|
`presence_exec` waits for the command, so it is the wrong tool for a training run, a
|
|
@@ -237,13 +270,23 @@ build or a server. The recipe:
|
|
|
237
270
|
|
|
238
271
|
```text
|
|
239
272
|
1. presence_exec { command: "python train.py --lr 3e-4", background: true, label: "train-3e-4" }
|
|
240
|
-
→ { id: "j7", pid: 4242, logPath: "
|
|
241
|
-
2. presence_jobs { action: "wait", jobId: "j7" }
|
|
242
|
-
3. presence_tail { path:
|
|
273
|
+
→ { id: "j7", pid: 4242, pgid: 4242, logPath: ".../job.log", commandLogPath: ".../job.out" }
|
|
274
|
+
2. presence_jobs { action: "wait", jobId: "j7" } # or presence_wait { job: { jobId: "j7" } }
|
|
275
|
+
3. presence_tail { path: <logPath>, follow: true } # watch progress
|
|
276
|
+
presence_wait { grep: { path: <logPath>, pattern: "epoch 6", count: 1 } } # readiness
|
|
243
277
|
```
|
|
244
278
|
|
|
245
279
|
- `presence_jobs { action: "status" }` gives a snapshot; `stop` terminates the process
|
|
246
|
-
group and verifies the port was released.
|
|
280
|
+
group and verifies the port was released. Jobs persist to `~/.dsh/presence/jobs.json`:
|
|
281
|
+
after a DSH restart a job shows `unknown` until a liveness probe succeeds (never a false
|
|
282
|
+
`running`) and its exit code may be missing.
|
|
283
|
+
- `logPath` is the job log to tail — the command's stdout/stderr on current kernels.
|
|
284
|
+
`commandLogPath` is the command's own log; it equals `logPath` unless the kernel records
|
|
285
|
+
a separate wrapper log, so tail `commandLogPath ?? logPath`.
|
|
286
|
+
- `presence_wait` replaces hand-written sleep+tail loops: wait for a port
|
|
287
|
+
(`presence_wait { port: { port: 8000, state: "open" } }`), a checkpoint
|
|
288
|
+
(`presence_wait { file: { path: "/data/ckpt.pt", minBytes: 1000000 } }`), a log line or
|
|
289
|
+
a process. A timeout returns `met:false` — a normal answer, not an error.
|
|
247
290
|
- Interactive work: `presence_terminal { action: "open", session: "train" }` — the
|
|
248
291
|
tmux-backed session survives a reconnect.
|
|
249
292
|
- **Do not** start `nohup`, `tmux new-session` or a trailing `&` inside a foreground
|
|
@@ -267,7 +310,11 @@ build or a server. The recipe:
|
|
|
267
310
|
- **Writes are atomic** and keep the original file mode; `presence_edit` refuses
|
|
268
311
|
ambiguous anchors instead of guessing.
|
|
269
312
|
- **Read before write**: built-in `read`/`write`/`edit` share a read-before-write
|
|
270
|
-
guard
|
|
313
|
+
guard. Path keys are canonicalised across `presence://`, mount paths, workspace-relative
|
|
314
|
+
and absolute remote paths, and successful `presence_read` / `presence_write` /
|
|
315
|
+
`presence_edit` calls count as a read (a write counts too). `presence_exec` running
|
|
316
|
+
`cat file` does **not** count — the guard cannot see shell output; read with
|
|
317
|
+
`presence_read` or the built-in `read` before editing.
|
|
271
318
|
|
|
272
319
|
## Configuration
|
|
273
320
|
|
|
@@ -275,8 +322,9 @@ The plugin reads these environment variables on the host process:
|
|
|
275
322
|
|
|
276
323
|
| Variable | Default | Meaning |
|
|
277
324
|
| --- | --- | --- |
|
|
278
|
-
| `PRESENCE_HOST` | — | Default SSH host (
|
|
325
|
+
| `PRESENCE_HOST` | — | Default SSH host for **local** sessions (explicit opt-in; also used by the MCP server). Remote-workspace sessions ignore it in favour of their mounted host. |
|
|
279
326
|
| `PRESENCE_WORKSPACE` | — | `host:/abs/path` to bind a remote workspace. |
|
|
327
|
+
| `PRESENCE_JOBS_FILE` | `~/.dsh/presence/jobs.json` | Override the persisted background-job registry path. |
|
|
280
328
|
| `PRESENCE_PASSWORD_<ALIAS>` | — | Password for a host; `<ALIAS>` is uppercase with non-alphanumerics replaced by `_`. |
|
|
281
329
|
| `PRESENCE_MAX_CHANNELS` | 8 | Concurrent channels per connection. |
|
|
282
330
|
| `PRESENCE_CONNECT_TIMEOUT_MS` | 25000 | Connection attempt timeout. |
|
|
@@ -294,8 +342,16 @@ npm install --prefix .dev --ignore-scripts rollup @rollup/plugin-node-resolve \
|
|
|
294
342
|
@rollup/plugin-commonjs @rollup/plugin-terser codemirror @codemirror/language-data \
|
|
295
343
|
@codemirror/theme-one-dark
|
|
296
344
|
node scripts/build-client.mjs
|
|
297
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package.ps1 -Version 0.
|
|
298
|
-
# → dist/presence-plugin-0.
|
|
345
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package.ps1 -Version 0.20.1
|
|
346
|
+
# → dist/presence-plugin-0.20.1.zip
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
After a DSH update, prove the plugin still activates against the shipped runtime
|
|
350
|
+
(Electron-as-Node; this is what caught the 0.2.1-rc.5 tool-schema tightening):
|
|
351
|
+
|
|
352
|
+
```powershell
|
|
353
|
+
$env:ELECTRON_RUN_AS_NODE = '1'
|
|
354
|
+
& 'C:\...\DeepSeek Harness.exe' scripts\host-compat-check.mjs # ACTIVATED OK with 25 tools
|
|
299
355
|
```
|
|
300
356
|
|
|
301
357
|
Tests need one SSH-reachable host unless noted:
|
|
@@ -307,17 +363,30 @@ node scripts/feature-probe.mjs server # wait, sessions, pgid, readiness, search
|
|
|
307
363
|
node scripts/import-probe.mjs server # local→remote binary, mode, sha256
|
|
308
364
|
node scripts/safety-probe.mjs server # destructive guard + observation store
|
|
309
365
|
node scripts/p0-guard-probe.mjs # guard split/partial/single-rm (no host needed)
|
|
366
|
+
node scripts/p1-docs-probe.mjs # README/zh/RELEASE anchors (no host needed)
|
|
310
367
|
```
|
|
311
368
|
|
|
312
369
|
## Known limits
|
|
313
370
|
|
|
371
|
+
- **Local sessions.** A local session calling `presence_*` without `host` now raises
|
|
372
|
+
`this session is local (cwd=...); pass an explicit host ...`. That is the intended 0.20
|
|
373
|
+
behavior, not a bug: the old silent fallback to the last-activated remote workspace could
|
|
374
|
+
run commands on another session's machine. Pass `host` per call, or set `PRESENCE_HOST`
|
|
375
|
+
once as the explicit opt-in default; a local session still never inherits another
|
|
376
|
+
session's workspace, and `presence_hosts`' `default` is session-scoped for the same reason.
|
|
377
|
+
- The read-before-write guard cannot observe shell output: `presence_exec { command:
|
|
378
|
+
"cat file" }` (or `sed -n`) does **not** mark the file as read; `presence_read` and the
|
|
379
|
+
built-in `read` do, and `presence_write`/`presence_edit` count as reads. A guarded
|
|
380
|
+
built-in `edit` after only `exec cat` can still fail.
|
|
314
381
|
- The mount path (`~/.dsh/presence/mounts/...`) is virtual: it works through
|
|
315
382
|
`ctx.fs` / agent tools, but native apps (Explorer, Office, host Python) cannot
|
|
316
383
|
see it — use `presence_export` first.
|
|
317
384
|
- `presence_exec` defaults to a 300 s timeout; longer work must use `background: true`
|
|
318
385
|
(foreground output over the cap is spilled to a remote file, and `tailLines` keeps the
|
|
319
|
-
end of a long report). Background jobs
|
|
320
|
-
DSH restart
|
|
386
|
+
end of a long report). Background jobs persist to `~/.dsh/presence/jobs.json`; after a
|
|
387
|
+
DSH restart their state is `unknown` until a liveness probe succeeds — never assume a
|
|
388
|
+
persisted job is still running, and its exit code may be missing. Tail
|
|
389
|
+
`commandLogPath ?? logPath` (`logPath` holds the command output on current kernels).
|
|
321
390
|
- A login node may clean up processes left behind when a session closes: do not rely on
|
|
322
391
|
`nohup` / `tmux new-session` / `&` started inside a foreground `presence_exec` for long
|
|
323
392
|
jobs. Use `background: true` (detached with `setsid`) or `presence_terminal` (tmux),
|
package/README.zh-CN.md
CHANGED
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
25
|
+
> ⚠️ **本地会话不会绑定远端工作区。** 在 Presence 里打开一个远程工作区,并不会把本地 agent
|
|
26
|
+
> 会话变成远端会话。当会话工作目录不在 presence 挂载根下时,`presence_exec` 及其他
|
|
27
|
+
> `presence_*` 工具必须显式传 `host`(或配置 `PRESENCE_HOST`);否则会带着当前 cwd 报错,
|
|
28
|
+
> 而不是静默跑到上一次激活的机器上。在远程工作区内启动的会话则自动解析到该机器。
|
|
29
|
+
> 详见[已知边界](#已知边界)。
|
|
30
|
+
|
|
25
31
|
## 为什么不是「agent 跑在远端」
|
|
26
32
|
|
|
27
33
|
在每台服务器上装 agent,意味着装 N 次、升级 N 次、逐台配代理;让本地 agent 每次
|
|
@@ -43,6 +49,10 @@ Presence 让 agent 留在本机,**通过每台机器一条常驻 SSH 连接把
|
|
|
43
49
|
- **用目标机的原生工具** —— `rg`/`grep` 在目标机跑,命令在目标 shell 里执行;路径、权限、编码都是远端的语义。
|
|
44
50
|
- **接近本地的编辑** —— 原子写、保留 mode、锚点歧义拒绝、读后写守卫、`presence_edit` 的 dry-run diff。
|
|
45
51
|
- **真正的远程工作区** —— 远端目录注册成普通 DSH 工作区,文件树、终端都**开在远端机器上**。
|
|
52
|
+
- **会话有作用域(0.20 热修)** —— 本地会话不再静默借用上一次激活的远程工作区:`presence_*` 必须显式传 `host` 或配置 `PRESENCE_HOST`;系统提示会告诉每个会话自己是本地还是远程工作区。
|
|
53
|
+
- **条件等待,不用 sleep** —— `presence_wait` 等文件(`exists`/`minBytes`/`mtimeAfter`)、grep 命中数、进程(`pid`/`pattern`)、端口状态或任务 id;超时返回 `met:false`,不是错误。
|
|
54
|
+
- **任务跨重启存活** —— 后台任务持久化到 `~/.dsh/presence/jobs.json`,带 pid/pgid;`logPath` 是要 tail 的任务日志,`commandLogPath` 是命令自己的日志(当前内核里等于 `logPath`;只有内核提供独立包装日志时才不同),用 `commandLogPath ?? logPath`;重启后状态是 `unknown` 直到探活,不会假装 `running`,退出码可能缺失。
|
|
55
|
+
- **批量传输与追加写** —— `presence_import`/`presence_export` 支持 `paths[]`(1–50)或 glob,逐文件返回;`presence_write { mode:"append" }` 追加写。
|
|
46
56
|
- **可恢复的删除** —— 危险 shell 命令先 dry-run;`presence_trash` 移入远端回收站并可恢复。
|
|
47
57
|
- **`@` 能补全远端文件** —— 远程工作区里,文件引用补全直接索引目标机(`rg --files`,回退 `find`),不是本地镜像。
|
|
48
58
|
- **文件操作不只是读** —— `presence_mkdir` / `move` / `copy` / `chmod`,文件树右键菜单直接操作;删除走可恢复回收站。
|
|
@@ -56,7 +66,7 @@ Presence 让 agent 留在本机,**通过每台机器一条常驻 SSH 连接把
|
|
|
56
66
|
### 1. 安装 DSH 插件
|
|
57
67
|
|
|
58
68
|
```sh
|
|
59
|
-
dsh plugin add @rjfan/dsh-presence@0.
|
|
69
|
+
dsh plugin add @rjfan/dsh-presence@0.20.1
|
|
60
70
|
```
|
|
61
71
|
|
|
62
72
|
从源码安装:
|
|
@@ -71,6 +81,9 @@ dsh plugin add /path/to/dsh-presence
|
|
|
71
81
|
`ssh` 命令),再选一个远端目录。它会以工作区出现:左侧文件树、终端和 agent 工具
|
|
72
82
|
都使用这个远端目录。
|
|
73
83
|
|
|
84
|
+
在该工作区里启动的会话就是远程会话;在其它地方(例如普通本地项目)启动的会话仍是本地
|
|
85
|
+
会话:调用 `presence_exec` / `presence_*` 前必须显式传 `host`,或配置 `PRESENCE_HOST`。
|
|
86
|
+
|
|
74
87
|
### 3. 开始用
|
|
75
88
|
|
|
76
89
|
```text
|
|
@@ -143,8 +156,9 @@ Linux 主机上的实测:
|
|
|
143
156
|
|
|
144
157
|
## 工具
|
|
145
158
|
|
|
146
|
-
DSH 插件提供
|
|
147
|
-
|
|
159
|
+
DSH 插件提供 25 个工具。在远程工作区会话里,相对路径相对该工作区、`host` 可省略;**本地**
|
|
160
|
+
会话必须显式传 `host`(或配置 `PRESENCE_HOST`)—— 不会回退到全局最后激活的工作区。执行时
|
|
161
|
+
接受参数别名(`path`/`file_path`/`filePath`、`old`/`oldString`、`new`/`newString`、
|
|
148
162
|
`cwd`/`workdir` 等),习惯写法都能用。
|
|
149
163
|
|
|
150
164
|
<details>
|
|
@@ -152,15 +166,15 @@ DSH 插件提供 24 个工具。相对路径相对当前远程工作区;在工
|
|
|
152
166
|
|
|
153
167
|
| 工具 | 说明 |
|
|
154
168
|
| --- | --- |
|
|
155
|
-
| `presence_hosts` | 列出 SSH 主机、连接状态和默认主机(支持 `format:"json"`);`workspace:true` 标记当前会话的机器,`degraded`
|
|
169
|
+
| `presence_hosts` | 列出 SSH 主机、连接状态和默认主机(支持 `format:"json"`);`workspace:true` 标记当前会话的机器,`degraded` 行带最后一次错误。`default` 是会话作用域:当前会话的挂载主机或 `PRESENCE_HOST`,绝不是全局最后激活的工作区。 |
|
|
156
170
|
| `presence_connect` | 建立/复用连接并返回远端身份。 |
|
|
157
|
-
| `presence_exec` | 执行远端命令;`pipefail`、`background`、`waitForPort`、结构化 JSON、`saveStdout`;超时默认 300 秒;`progress` 归一化 `\r` 进度条;超长输出落盘到远端文件并返回路径(`tailLines
|
|
158
|
-
| `presence_jobs` | 后台任务:`list`、`status`、`wait`(含 `timedOut`)、`stop`(`alive`/`verified`/`portReleased
|
|
171
|
+
| `presence_exec` | 执行远端命令;`pipefail`、`background`、`waitForPort`、结构化 JSON、`saveStdout`;超时默认 300 秒;`progress` 归一化 `\r` 进度条;超长输出落盘到远端文件并返回路径(`tailLines`);文本报告以 `host: <alias> remote cwd: <abs>` 开头;危险命令先 dry-run —— `confirm:true` 执行整条命令,`confirm:"partial"` 只执行安全分段。本地会话必须传 `host`。 |
|
|
172
|
+
| `presence_jobs` | 后台任务:`list`、`status`、`wait`(含 `timedOut`)、`stop`(`alive`/`verified`/`portReleased`)、`purge`。任务持久化到 `~/.dsh/presence/jobs.json`,含 `pid`/`pgid`;`logPath` 是要 tail 的任务日志(当前内核里即命令 stdout/stderr),`commandLogPath` 是命令自己的日志(当前内核里等于 `logPath`,用 `commandLogPath ?? logPath`)。重启后状态为 `unknown` 直到探活成功,退出码可能缺失。`/presence/api/state` 的 job 行还带 `pid`、`pgid`、`commandLogPath`、`persisted`。 |
|
|
173
|
+
| `presence_wait` | 阻塞直到某个远端条件成立(五选一):`file {path, exists?, minBytes?, mtimeAfter?}`、`grep {path, pattern, count?}`、`process {pid\|pattern, alive?}`、`port {port, state:"open"\|"closed"}`、`job {jobId}`(job 条件不需要 `host`,注册表知道它跑在哪台机器)。返回 `{ met, timedOut, elapsedMs, observed, condition, polls }`;超时是 `met:false`,**不会报错**。`timeoutMs` 默认 120 秒(上限 1 小时),`pollMs`/`intervalMs` 默认 1000 ms。 |
|
|
159
174
|
| `presence_read` | 读远端文件,带行号、`offset`/`limit`,`raw:true` 返回逐字节原样内容。 |
|
|
160
175
|
| `presence_read_many` | 一次读 1–50 个文件,支持每文件字节/行数上限。 |
|
|
161
|
-
| `presence_write` | 原子写;保留 mode;返回真实 mode、字节数和完整 sha256;`ifMatchSha256`
|
|
176
|
+
| `presence_write` | 原子写;保留 mode;返回真实 mode、字节数和完整 sha256;`ifMatchSha256` 乐观锁;`mode:"append"` 在 EOF 追加,返回追加字节数和总大小(`ifMatchSha256` 仅 overwrite 有效,append 传入会直接报错);`mode` 也可传八进制字符串(如 `"755"`)设置 overwrite 权限。 |
|
|
162
177
|
| `presence_edit` | 精确文本替换或行号区间替换,带紧凑 diff、`dryRun` 和 JSON 字段。 |
|
|
163
|
-
| `presence_ls` | 列目录;`recursive`、`totalBytes`、JSON 输出。 |
|
|
164
178
|
| `presence_stat` | 批量 `stat` 1–100 个路径:类型、大小、mode、mtime、可选 sha256;缺失路径返回 `exists:false`。 |
|
|
165
179
|
| `presence_mkdir` | 创建远端目录(默认递归);已存在时幂等成功。 |
|
|
166
180
|
| `presence_move` | 同一台机器内重命名/移动;目标已存在时除非 `overwrite:true` 否则拒绝。 |
|
|
@@ -170,8 +184,8 @@ DSH 插件提供 24 个工具。相对路径相对当前远程工作区;在工
|
|
|
170
184
|
| `presence_tail` | 读远端文件最后 N 行,或在限定时间内跟随(`follow:true`、`timeoutMs`)。 |
|
|
171
185
|
| `presence_search` | 远端 `rg`/`grep` 搜索,`countOnly`,JSON matches。 |
|
|
172
186
|
| `presence_glob` | 远端 glob 找文件,带 size/mtime。 |
|
|
173
|
-
| `presence_import` | 本地 → 远端二进制安全上传,可设 mode,返回 sha256
|
|
174
|
-
| `presence_export` | 远端 →
|
|
187
|
+
| `presence_import` | 本地 → 远端二进制安全上传,可设 mode,返回 sha256;支持批量 `paths[]`(1–50)和/或本地 `glob` 到 `to` 目录,逐文件返回结果。 |
|
|
188
|
+
| `presence_export` | 远端 → 本地真实文件(内容寻址),供预览或原生程序打开;支持批量 `paths[]` 或远端 `glob`;默认本地文件名取 basename,重名变成 `name-<sha8>.ext`;逐文件返回结果。 |
|
|
175
189
|
| `presence_forward` | 远端端口转发到 `127.0.0.1`;open/list/close 支持 JSON。 |
|
|
176
190
|
| `presence_port_probe` | 检查最多 20 个端口的监听与 TCP 可达性;`expect:open|closed` 可轮询。 |
|
|
177
191
|
| `presence_http_probe` | 等端口 → 开转发 → 本地 HTTP 断言 → 关转发 → 验证本地端口已释放。 |
|
|
@@ -180,26 +194,49 @@ DSH 插件提供 24 个工具。相对路径相对当前远程工作区;在工
|
|
|
180
194
|
|
|
181
195
|
</details>
|
|
182
196
|
|
|
197
|
+
### 批量传输与追加写
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
presence_export { paths: ["/data/a.pt", "/data/b.pt"] } # files:[{remotePath, localPath, bytes, sha256}]
|
|
201
|
+
presence_export { glob: "/data/ckpt-*.pt" } # 全部匹配;重名变成 name-<sha8>.ext
|
|
202
|
+
presence_import { glob: "dist/*.whl", to: "/data/wheels" } # 本地 → 远端,逐文件结果
|
|
203
|
+
presence_write { path: "/data/train.log", mode: "append", content: "epoch 6 done\n" }
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
单文件仍走原来的 `path`/`to`,上面的写法都不是必需的。
|
|
207
|
+
|
|
183
208
|
## 路径与工具怎么选
|
|
184
209
|
|
|
185
210
|
| 路径在哪 | 用什么 | 说明 |
|
|
186
211
|
| --- | --- | --- |
|
|
187
212
|
| 工作区挂载目录下(`~/.dsh/presence/mounts/...`) | 内置 `read` / `write` / `edit` / `grep` / `glob` | 都直达远端机器;`grep`/`glob` 会自动路由过去 |
|
|
188
|
-
| 工作区之外的远端绝对路径(`/data`、`/scratch`、GPFS) | `presence_read` / `write` / `edit` / `
|
|
213
|
+
| 工作区之外的远端绝对路径(`/data`、`/scratch`、GPFS) | `presence_read` / `write` / `edit` / `stat` / `search` / `glob`(列目录用 `presence_exec`) | 内置工具只看得到挂载目录,看不到整台机器 |
|
|
189
214
|
| 本地文件(预览或 Office) | 先 `presence_export`,再 `present` / 原生打开 | 挂载路径是虚拟的,只有真实本地文件能被原生程序打开 |
|
|
190
215
|
|
|
216
|
+
**本地**会话(cwd 不在挂载根下)里内置工具只操作本地文件;每个 `presence_*` 调用都需要显式
|
|
217
|
+
`host`,或一次性配置 `PRESENCE_HOST`。
|
|
218
|
+
|
|
191
219
|
## 长时间任务
|
|
192
220
|
|
|
193
221
|
`presence_exec` 会等命令结束,所以训练、构建、起服务都不该直接用它。推荐流程:
|
|
194
222
|
|
|
195
223
|
```text
|
|
196
224
|
1. presence_exec { command: "python train.py --lr 3e-4", background: true, label: "train-3e-4" }
|
|
197
|
-
→ { id: "j7", pid: 4242, logPath: "
|
|
198
|
-
2. presence_jobs { action: "wait", jobId: "j7" }
|
|
199
|
-
3. presence_tail { path:
|
|
225
|
+
→ { id: "j7", pid: 4242, pgid: 4242, logPath: ".../job.log", commandLogPath: ".../job.out" }
|
|
226
|
+
2. presence_jobs { action: "wait", jobId: "j7" } # 或 presence_wait { job: { jobId: "j7" } }
|
|
227
|
+
3. presence_tail { path: <logPath>, follow: true } # 看进度
|
|
228
|
+
presence_wait { grep: { path: <logPath>, pattern: "epoch 6", count: 1 } } # 等就绪
|
|
200
229
|
```
|
|
201
230
|
|
|
202
|
-
- `presence_jobs { action: "status" }` 取快照;`stop`
|
|
231
|
+
- `presence_jobs { action: "status" }` 取快照;`stop` 结束进程组并验证端口已释放。任务持久化到
|
|
232
|
+
`~/.dsh/presence/jobs.json`:DSH 重启后先显示 `unknown`,直到探活成功(不会假装 `running`),
|
|
233
|
+
退出码可能缺失。
|
|
234
|
+
- `logPath` 是要 tail 的任务日志(当前内核里就是命令输出);`commandLogPath` 是命令自己的日志,
|
|
235
|
+
没有独立包装日志时等于 `logPath`,所以用 `commandLogPath ?? logPath`。
|
|
236
|
+
- `presence_wait` 取代手写 sleep+tail 循环:等端口
|
|
237
|
+
(`presence_wait { port: { port: 8000, state: "open" } }`)、等 checkpoint
|
|
238
|
+
(`presence_wait { file: { path: "/data/ckpt.pt", minBytes: 1000000 } }`)、等日志行或等进程。
|
|
239
|
+
超时返回 `met:false`,是正常结果而不是错误。
|
|
203
240
|
- 交互式任务:`presence_terminal { action: "open", session: "train" }`,tmux 会话跨重连存活。
|
|
204
241
|
- **不要**在前台 `presence_exec` 里用 `nohup`、`tmux new-session` 或结尾 `&` 跑长任务:登录节点
|
|
205
242
|
会话结束时,集群可能清理残留进程。`background: true` 本身用 `setsid` 脱离;调度器管理的集群
|
|
@@ -215,7 +252,10 @@ DSH 插件提供 24 个工具。相对路径相对当前远程工作区;在工
|
|
|
215
252
|
(它不可能删掉一棵树)。
|
|
216
253
|
- **工作区内删除优先 `presence_trash`**:移动到 `~/.dsh/presence-trash/<时间戳>/` 并返回恢复提示。
|
|
217
254
|
- **写入是原子的**,保留原文件 mode;`presence_edit` 锚点不唯一时报错而不是猜。
|
|
218
|
-
- **读后写守卫**:内置 `read`/`write`/`edit`
|
|
255
|
+
- **读后写守卫**:内置 `read`/`write`/`edit` 共享读后写守卫。路径 key 会在 `presence://`、挂载路径、
|
|
256
|
+
工作区相对路径、远端绝对路径之间归一;`presence_read` / `presence_write` / `presence_edit` 成功都算
|
|
257
|
+
「已读」(写也算)。`presence_exec` 里 `cat file` **不算** —— 守卫看不到 shell 输出;编辑前请用
|
|
258
|
+
`presence_read` 或内置 `read`。
|
|
219
259
|
|
|
220
260
|
## 配置
|
|
221
261
|
|
|
@@ -223,8 +263,9 @@ DSH 插件提供 24 个工具。相对路径相对当前远程工作区;在工
|
|
|
223
263
|
|
|
224
264
|
| 变量 | 默认 | 含义 |
|
|
225
265
|
| --- | --- | --- |
|
|
226
|
-
| `PRESENCE_HOST` | — |
|
|
266
|
+
| `PRESENCE_HOST` | — | **本地**会话的默认 SSH 主机(显式 opt-in;MCP server 也用)。远程工作区会话优先使用自己的挂载主机。 |
|
|
227
267
|
| `PRESENCE_WORKSPACE` | — | `host:/abs/path`,绑定一个远程工作区。 |
|
|
268
|
+
| `PRESENCE_JOBS_FILE` | `~/.dsh/presence/jobs.json` | 覆盖后台任务持久化文件路径。 |
|
|
228
269
|
| `PRESENCE_PASSWORD_<ALIAS>` | — | 某台机器的密码;`<ALIAS>` 大写、非字母数字换成 `_`。 |
|
|
229
270
|
| `PRESENCE_MAX_CHANNELS` | 8 | 每连接并发通道数。 |
|
|
230
271
|
| `PRESENCE_CONNECT_TIMEOUT_MS` | 25000 | 连接尝试超时。 |
|
|
@@ -242,8 +283,16 @@ npm install --prefix .dev --ignore-scripts rollup @rollup/plugin-node-resolve \
|
|
|
242
283
|
@rollup/plugin-commonjs @rollup/plugin-terser codemirror @codemirror/language-data \
|
|
243
284
|
@codemirror/theme-one-dark
|
|
244
285
|
node scripts/build-client.mjs
|
|
245
|
-
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package.ps1 -Version 0.
|
|
246
|
-
# → dist/presence-plugin-0.
|
|
286
|
+
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/package.ps1 -Version 0.20.1
|
|
287
|
+
# → dist/presence-plugin-0.20.1.zip
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
DSH 升级后,用 Electron-as-Node 对着真实 Harness 运行时验证插件仍能激活
|
|
291
|
+
(0.2.1-rc.5 的工具 schema 收紧就是这一步发现的):
|
|
292
|
+
|
|
293
|
+
```powershell
|
|
294
|
+
$env:ELECTRON_RUN_AS_NODE = '1'
|
|
295
|
+
& 'C:\...\DeepSeek Harness.exe' scripts\host-compat-check.mjs # ACTIVATED OK with 25 tools
|
|
247
296
|
```
|
|
248
297
|
|
|
249
298
|
验证脚本需要一台可 SSH 的主机(标注“不需要真机”的除外):
|
|
@@ -255,14 +304,26 @@ node scripts/feature-probe.mjs server # wait、会话、pgid、就绪、搜索
|
|
|
255
304
|
node scripts/import-probe.mjs server # 本地→远端二进制、mode、sha256
|
|
256
305
|
node scripts/safety-probe.mjs server # 危险命令护栏 + 观测状态
|
|
257
306
|
node scripts/p0-guard-probe.mjs # 危险命令分段/单文件豁免(不需要真机)
|
|
307
|
+
node scripts/p1-docs-probe.mjs # README/zh/RELEASE 锚点(不需要真机)
|
|
258
308
|
```
|
|
259
309
|
|
|
260
310
|
## 已知边界
|
|
261
311
|
|
|
312
|
+
- **本地会话(有意行为)。** 本地会话不传 `host` 调 `presence_*` 现在会报
|
|
313
|
+
`this session is local (cwd=...); pass an explicit host ...` —— 这是 0.20 有意的行为,不是 bug:
|
|
314
|
+
旧逻辑会静默复用上一次激活的远程工作区,可能在别的会话的机器上执行命令。每次调用传 `host`,
|
|
315
|
+
或一次性配置 `PRESENCE_HOST` 作为显式默认值;本地会话仍然不会继承别的工作区,
|
|
316
|
+
`presence_hosts` 的 `default` 同样是会话作用域。
|
|
317
|
+
- 读后写守卫看不到 shell 输出:`presence_exec { command: "cat file" }`(或 `sed -n`)**不会**把
|
|
318
|
+
文件标记为已读;`presence_read` 和内置 `read` 才算,`presence_write`/`presence_edit` 也算已读。
|
|
319
|
+
只 `exec cat` 过再让内置 `edit` 编辑,仍可能触发守卫。
|
|
262
320
|
- 挂载路径(`~/.dsh/presence/mounts/...`)是虚拟的:对 `ctx.fs` / agent 工具有效,
|
|
263
321
|
但 Explorer、Office、宿主 Python 看不到 —— 需要先用 `presence_export`。
|
|
264
322
|
- `presence_exec` 超时默认 300 秒;更长的任务必须用 `background: true`(前台输出超限会
|
|
265
|
-
落盘到远端文件,`tailLines`
|
|
323
|
+
落盘到远端文件,`tailLines` 保留长报告末尾)。后台任务持久化到
|
|
324
|
+
`~/.dsh/presence/jobs.json`;DSH 重启后状态是 `unknown` 直到探活成功 —— 不要把持久化记录
|
|
325
|
+
当成还在运行,退出码可能缺失。要 tail 的日志用 `commandLogPath ?? logPath`(当前内核里
|
|
326
|
+
`logPath` 就是命令输出)。
|
|
266
327
|
- 登录节点可能在会话结束时清理残留进程:不要在前台 `presence_exec` 里用 `nohup` /
|
|
267
328
|
`tmux new-session` / `&` 跑长任务。用 `background: true`(`setsid` 脱离)或
|
|
268
329
|
`presence_terminal`(tmux);集群上让调度器持有任务(`rlaunch -d`、`salloc`/`sbatch`)。
|