From bf893c67a7689ba0d310e3e8868674b5d0dbc807 Mon Sep 17 00:00:00 2001 From: schickling-assistant <261620128+schickling-assistant@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:10:30 +0200 Subject: [PATCH 1/3] feat: patch session metadata atomically --- CHANGELOG.md | 16 +++ README.md | 15 +++ completions/pty.bash | 5 +- completions/pty.fish | 3 + completions/pty.zsh | 6 + docs/client.md | 34 +++++ docs/disk-layout.md | 3 +- src/cli.ts | 81 ++++++++++++ src/client-api.ts | 7 +- src/completions.ts | 6 + src/events.ts | 18 ++- src/sessions.ts | 242 ++++++++++++++++++++++++++-------- tests/help.test.ts | 2 +- tests/metadata-events.test.ts | 167 ++++++++++++++++++++++- 14 files changed, 539 insertions(+), 66 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf93201..b88424e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ ## Unreleased +### Atomic exact-id metadata patching + +- `pty metadata patch --id ` reads one merge-style JSON object from + stdin and atomically updates `displayName` and tags under one metadata lock. + It never falls back to display-name lookup, preserves unrelated tags, returns + `{ changed, metadata }`, and suppresses no-op writes and events. +- `patchMetadataById(id, patch)` exposes the same exact-id operation from + `@compoundingtech/pty/client`. Existing rename/tag APIs share the merge engine + while retaining their documented specialized events for compatibility. + +### Storage format + +Effective atomic patches append one `metadata_change` event whose `previous` +and `value` objects contain only changed `displayName` and tag keys. No-op +patches append no event. + ### Non-unique display names with unambiguous session resolution - Display names are presentation metadata and no longer need to be unique. diff --git a/README.md b/README.md index 68570fb..45f758d 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ pty rename my-label # inside a session: add/change its dis pty rename my-label # outside: set displayName on pty rename --show # show current displayName pty rename --clear [ref] # remove displayName +pty metadata patch --id myserver < patch.json # atomically patch displayName/tags by exact id pty list # show active sessions (tags shown by default) pty list --tags # include internal bookkeeping tags (ptyfile*, strategy, etc.) @@ -125,6 +126,20 @@ exact stable id first, then a display name only when that label has one match. Ambiguous display names fail without acting and print the candidate stable ids. Use stable ids in scripts and automation. +For automation that must update presentation metadata without alias fallback, +`pty metadata patch --id ` reads one merge-style JSON object from +stdin and returns `{ changed, metadata }` as JSON: + +```sh +printf '%s' '{"displayName":"Worker","tags":{"role":"worker","old":null}}' \ + | pty metadata patch --id a1b2c3d4 +``` + +`displayName` and individual tag values use strings to set and `null` to clear; +omitted fields and tag keys remain unchanged. The operation holds the session's +metadata lock across one read/merge/atomic-write cycle. It fails if the exact id +is absent, even when a display name has the same text. + ### Remote over fabric `pty list --remote ` lists another machine's sessions over [fabric](https://github.com/compoundingtech/fabric), which hands consumers a plain local Unix socket — pty never touches iroh. The remote machine serves a small control protocol that fabric exposes under the `pty-remote` ALPN. The recommended form is **on-demand**: fabric spawns the handler per dial, pipes the connection to its stdin/stdout, and owns persistence + roaming (no persistent pty daemon): diff --git a/completions/pty.bash b/completions/pty.bash index d9c17d8..4bfe51d 100644 --- a/completions/pty.bash +++ b/completions/pty.bash @@ -5,7 +5,7 @@ _pty() { COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" - commands="run attach a exec peek send events list ls stats restart kill rm remove gc tag tag-multi emit rename up down test remote-serve" + commands="run attach a exec peek send events list ls stats restart kill rm remove gc tag tag-multi emit rename metadata up down test remote-serve" if [[ ${COMP_CWORD} -eq 1 ]]; then if [[ "${cur}" == -* ]]; then @@ -119,6 +119,9 @@ _pty() { COMPREPLY=($(compgen -W "${names}" -- "${cur}")) fi ;; + metadata) + COMPREPLY=($(compgen -W "--id" -- "${cur}")) + ;; up) COMPREPLY=($(compgen -o dirnames -- "${cur}")) ;; diff --git a/completions/pty.fish b/completions/pty.fish index 78c6c27..f4570d3 100644 --- a/completions/pty.fish +++ b/completions/pty.fish @@ -60,6 +60,7 @@ complete -c pty -n __pty_needs_command -a tag -d 'Read / write tags on one sessi complete -c pty -n __pty_needs_command -a tag-multi -d 'Bulk tag ops across sessions' complete -c pty -n __pty_needs_command -a emit -d 'Publish a user.* event' complete -c pty -n __pty_needs_command -a rename -d 'Set / show / clear displayName' +complete -c pty -n __pty_needs_command -a metadata -d 'Atomically patch presentation metadata by stable id' complete -c pty -n __pty_needs_command -a up -d 'Start sessions from pty.toml' complete -c pty -n __pty_needs_command -a down -d 'Stop sessions from pty.toml' complete -c pty -n __pty_needs_command -a test -d 'Run the pty test suite (vitest)' @@ -135,6 +136,8 @@ complete -c pty -n '__pty_using_command emit' -a '(__pty_sessions)' -d 'Session' complete -c pty -n '__pty_using_command rename' -l show -d 'Print current displayName' complete -c pty -n '__pty_using_command rename' -l clear -d 'Remove displayName' complete -c pty -n '__pty_using_command rename' -a '(__pty_sessions)' -d 'Session' +complete -c pty -n '__pty_using_command metadata' -l id -d 'Exact stable session id' +complete -c pty -n '__pty_using_command metadata' -x -a 'patch' -d 'Value' complete -c pty -n '__pty_using_command up' -F complete -c pty -n '__pty_using_command down' -F complete -c pty -n '__pty_using_command test' -l t -d 'Run matching tests' diff --git a/completions/pty.zsh b/completions/pty.zsh index 84cb4ac..93755e5 100644 --- a/completions/pty.zsh +++ b/completions/pty.zsh @@ -33,6 +33,7 @@ _pty() { 'tag-multi:Bulk tag ops across sessions' 'emit:Publish a user.* event' 'rename:Set / show / clear displayName' + 'metadata:Atomically patch presentation metadata by stable id' 'up:Start sessions from pty.toml' 'down:Stop sessions from pty.toml' 'test:Run the pty test suite (vitest)' @@ -171,6 +172,11 @@ _pty() { '--clear[Remove displayName]' \ '1:session:_pty_sessions' ;; + metadata) + _arguments \ + '--id[Exact stable session id]' \ + '1:mode:(patch)' + ;; up) _arguments \ '1:directory:_directories' diff --git a/docs/client.md b/docs/client.md index 461a502..88a5cfb 100644 --- a/docs/client.md +++ b/docs/client.md @@ -27,6 +27,36 @@ matches. Resolve once, then pass `session.name` to socket-oriented APIs. Throws if the name is invalid. Names must match `[a-zA-Z0-9._-]` and be at most 255 characters. +### `patchMetadataById(id: string, patch: MetadataPatch): Promise` + +Atomically merge presentation metadata for one exact stable id. This API never +falls back to a matching display name. It holds the session metadata lock across +one read, merge, validation, and atomic write; unrelated tags are preserved and +a no-op returns `changed: false` without writing or emitting an event. + +```typescript +const result = await patchMetadataById("a1b2c3d4", { + displayName: "Worker", + tags: { role: "worker", temporary: null }, +}); + +interface MetadataPatch { + displayName?: string | null; + tags?: Record; +} + +interface MetadataPatchResult { + changed: boolean; + metadata: SessionMetadata; +} +``` + +Strings set values, `null` clears them, and omitted fields or tag keys remain +unchanged. A successful change emits one `metadata_change` event containing +only effective changes as `previous` and `value` snapshots. The existing +`setDisplayName` and `updateTags` APIs retain their specialized event types for +compatibility. + ### `getSessionDir(): string` Returns the session directory path — `$PTY_ROOT` if set (the legacy `$PTY_SESSION_DIR` name is still honored), otherwise `~/.local/state/pty`. @@ -382,6 +412,10 @@ Each extends `EventBase { session: string; type: EventType; ts: string }`. `NotificationEvent` adds `title?`, `body?`, `source?: "osc9" | "osc99" | "osc777"`. `TitleChangeEvent` adds `value: string`. +`MetadataChangeEvent` has type `"metadata_change"` and carries `previous` and +`value` objects. Only the changed `displayName` field and changed tag keys are +present; `null` represents an absent or cleared value. + ## Keys (also available via `@compoundingtech/pty/keys`) These functions are also available as a standalone browser-safe import via `@compoundingtech/pty/keys` (zero dependencies). diff --git a/docs/disk-layout.md b/docs/disk-layout.md index 5d9f838..cb5d2a9 100644 --- a/docs/disk-layout.md +++ b/docs/disk-layout.md @@ -94,6 +94,7 @@ Envelope: `{ session: string; type: string; ts: string; ...payload }`. Event typ | `session_flapping` | `counter, limit, window` — (`pty gc` flipped a permanent session to `strategy.status=flapping` after N consecutive fast-fail respawns; subsequent ticks skip it) | | `display_name_change` | `previous: string\|null, value: string\|null` | | `tags_change` | `previous, value` (full snapshots) | +| `metadata_change` | `previous, value` containing only changed `displayName` and tag keys; absent tag values are `null` | | `user.` | `data?, text?` — free-form, via `pty emit` | A single line ≤ `PIPE_BUF` (~4 KB) is atomic per POSIX `O_APPEND`. Built-ins are well under. Keep large `user.*` payloads out of the event stream. @@ -104,4 +105,4 @@ A single line ≤ `PIPE_BUF` (~4 KB) is atomic per POSIX `O_APPEND`. Built-ins a jq -r '.tags["role"] // empty' "$PTY_ROOT/myserver.json" ``` -For live updates, tail `.events.jsonl` via `inotify` / `kqueue`. Subscribe instead of polling — `tags_change` / `display_name_change` / `session_*` fire on every mutation. +For live updates, tail `.events.jsonl` via `inotify` / `kqueue`. Subscribe instead of polling — `metadata_change` / `tags_change` / `display_name_change` / `session_*` fire on every mutation. diff --git a/src/cli.ts b/src/cli.ts index 2a9ff72..2702cf6 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -25,6 +25,7 @@ import { releaseLock, updateTags, setDisplayName, + patchMetadataById, allSessionNames, readMetadata, readSessionPid, @@ -356,6 +357,20 @@ Examples: pty rename webapp "Web Frontend" pty rename --show webapp`, + metadata: `Usage: pty metadata patch --id + +Atomically merge displayName and tags for one exact stable session id. Reads +one JSON object from stdin; it never resolves display-name aliases. + +Patch fields: + displayName string to set, null to clear, omitted to preserve + tags object of string values to set and null values to remove + +Examples: + pty metadata patch --id a1b2c3d4 < patch.json + printf '%s' '{"displayName":"Worker","tags":{"role":"worker"}}' | pty metadata patch --id a1b2c3d4 + printf '%s' '{"displayName":null,"tags":{"temporary":null}}' | pty metadata patch --id a1b2c3d4`, + up: `Usage: pty up [] [...] Start sessions declared in a pty.toml. With no args, reads ./pty.toml and starts all. @@ -452,6 +467,7 @@ Observe: pty remote-serve --socket Serve remote access as a listening daemon (being retired) Modify: + pty metadata patch --id Atomically merge displayName/tags from JSON stdin pty rename