@timurproko/a1 0.2.7-dev.792 → 0.2.7-dev.801
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/dist/app/session-shell/clipboard-diagnostics.js +1 -1
- package/dist/app/session-shell/response-copy-helper.js +21 -6
- package/dist/app/session-shell/response-copy-protocol.d.ts +1 -1
- package/dist/app/session-shell/response-copy-transport.d.ts +3 -6
- package/dist/app/session-shell/response-copy-transport.js +60 -17
- package/dist/app/session-shell/session-shell-root.d.ts +2 -0
- package/dist/app/session-shell/session-shell-root.js +6 -1
- package/dist/app/session-shell/session-shell.d.ts +1 -0
- package/dist/app/session-shell/session-shell.js +22 -19
- package/dist/app/session-shell/shell-host.js +11 -0
- package/dist/cli/tabs.js +1 -1
- package/dist/composition/owned-ui.d.ts +1 -0
- package/dist/composition/owned-ui.js +16 -3
- package/dist/features/resident-tab/shortcut-declarations.d.ts +1 -1
- package/dist/features/resident-tab/shortcut-declarations.js +1 -0
- package/dist/foundation/launch-guardian/exit-watcher.d.ts +14 -0
- package/dist/foundation/launch-guardian/exit-watcher.js +94 -0
- package/dist/foundation/launch-guardian/main.d.ts +2 -1
- package/dist/foundation/launch-guardian/main.js +58 -6
- package/dist/foundation/process-containment/{native-guardian-containment.d.ts → addon-containment.d.ts} +7 -5
- package/dist/foundation/process-containment/addon-containment.js +139 -0
- package/dist/foundation/process-containment/addon-process-inspector.d.ts +11 -0
- package/dist/foundation/process-containment/addon-process-inspector.js +36 -0
- package/dist/foundation/process-containment/addon.d.ts +42 -0
- package/dist/foundation/process-containment/addon.js +57 -0
- package/dist/foundation/process-containment/artifact.d.ts +1 -1
- package/dist/foundation/process-containment/artifact.js +6 -5
- package/dist/foundation/process-containment/contracts.d.ts +2 -0
- package/dist/foundation/process-containment/index.d.ts +4 -4
- package/dist/foundation/process-containment/index.js +3 -3
- package/dist/foundation/process-containment/native-process-inspector.d.ts +2 -1
- package/dist/foundation/process-containment/native-process-inspector.js +10 -10
- package/dist/foundation/release/bootstrap.js +53 -16
- package/dist/foundation/release/index.d.ts +1 -1
- package/dist/foundation/release/index.js +1 -1
- package/dist/foundation/release/release-store.d.ts +12 -0
- package/dist/foundation/release/release-store.js +124 -2
- package/dist/foundation/resident-tabs/bounded-host-command.d.ts +3 -1
- package/dist/foundation/resident-tabs/bounded-host-command.js +6 -2
- package/dist/foundation/resident-tabs/bridge-client.d.ts +2 -0
- package/dist/foundation/resident-tabs/bridge-client.js +13 -0
- package/dist/foundation/resident-tabs/doctor.d.ts +0 -1
- package/dist/foundation/resident-tabs/doctor.js +4 -4
- package/dist/foundation/resident-tabs/ensure-terminal-host.d.ts +7 -2
- package/dist/foundation/resident-tabs/ensure-terminal-host.js +15 -11
- package/dist/foundation/resident-tabs/fixture-control.d.ts +7 -0
- package/dist/foundation/resident-tabs/fixture-control.js +35 -0
- package/dist/foundation/resident-tabs/host-entry.d.ts +1 -0
- package/dist/foundation/resident-tabs/host-entry.js +17 -0
- package/dist/foundation/resident-tabs/index.d.ts +2 -2
- package/dist/foundation/resident-tabs/index.js +1 -1
- package/dist/foundation/resident-tabs/maintenance.d.ts +5 -4
- package/dist/foundation/resident-tabs/maintenance.js +32 -17
- package/dist/foundation/resident-tabs/progress-observation.d.ts +7 -0
- package/dist/foundation/resident-tabs/progress-observation.js +119 -0
- package/dist/foundation/resident-tabs/release-holds.d.ts +3 -2
- package/dist/foundation/resident-tabs/release-holds.js +16 -7
- package/dist/foundation/resident-tabs/settings.d.ts +0 -1
- package/dist/foundation/resident-tabs/settings.js +1 -7
- package/dist/foundation/resident-tabs/terminal-host-artifact.d.ts +11 -15
- package/dist/foundation/resident-tabs/terminal-host-artifact.js +57 -32
- package/dist/foundation/session-writer-lock/index.d.ts +1 -3
- package/dist/foundation/session-writer-lock/index.js +8 -6
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +2 -1
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +2 -0
- package/dist/integrations/pi/engine/session-runtime.js +5 -2
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.d.ts +11 -0
- package/dist/integrations/pi/tui-runtime/damage-aware-terminal.js +30 -1
- package/dist/native/darwin-arm64/manifest.json +8 -6
- package/dist/native/darwin-arm64/process-guardian.node +0 -0
- package/dist/native/darwin-arm64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +14 -13
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +9 -7
- package/dist/native/darwin-x64/process-guardian.node +0 -0
- package/dist/native/darwin-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +15 -14
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +8 -6
- package/dist/native/linux-x64/process-guardian.node +0 -0
- package/dist/native/linux-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +14 -13
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +8 -6
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host-THIRD-PARTY-NOTICES.txt +1 -1
- package/dist/native/win32-x64/terminal-host.manifest.json +14 -13
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/dist/product-identity.d.ts +3 -3
- package/dist/product-identity.js +3 -3
- package/dist/product-identity.json +9 -2
- package/dist/runtime-payload-inventory.json +1 -0
- package/dist/ui/settings/declarations.d.ts +0 -12
- package/dist/ui/settings/declarations.js +0 -10
- package/docs/architecture/boundaries.md +10 -9
- package/docs/architecture/process-guardian-provenance.md +17 -11
- package/docs/architecture/resident-tabs.md +46 -40
- package/docs/architecture/terminal-host-provenance.md +5 -3
- package/docs/architecture/toolchain.md +8 -6
- package/docs/ci-release-runbook.md +1 -1
- package/docs/openspec-archive-automation.md +23 -9
- package/docs/validation.md +6 -2
- package/package.json +6 -3
- package/dist/foundation/process-containment/darwin-process-inspector.d.ts +0 -5
- package/dist/foundation/process-containment/darwin-process-inspector.js +0 -7
- package/dist/foundation/process-containment/native-guardian-containment.js +0 -152
- package/dist/foundation/process-containment/windows-process-inspector.d.ts +0 -19
- package/dist/foundation/process-containment/windows-process-inspector.js +0 -56
- package/dist/native/darwin-arm64/a1_session_lock.node +0 -0
- package/dist/native/darwin-arm64/process-guardian +0 -0
- package/dist/native/darwin-arm64/terminal-host +0 -0
- package/dist/native/darwin-x64/a1_session_lock.node +0 -0
- package/dist/native/darwin-x64/process-guardian +0 -0
- package/dist/native/darwin-x64/terminal-host +0 -0
- package/dist/native/linux-x64/a1_session_lock.node +0 -0
- package/dist/native/linux-x64/process-guardian +0 -0
- package/dist/native/linux-x64/terminal-host +0 -0
- package/dist/native/win32-x64/a1_session_lock.node +0 -0
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/native/win32-x64/terminal-host.exe +0 -0
|
@@ -1,29 +1,35 @@
|
|
|
1
1
|
# Process guardian provenance
|
|
2
2
|
|
|
3
|
-
A1 launch-instance containment uses
|
|
3
|
+
A1 launch-instance containment uses the Node-API 9 addon `process-guardian.node`. The approved implementation is the separate Rust crate at `native/process-guardian`; it produces no product executable. The foreground Node launch guardian loads the addon and holds the containment boundary in-process.
|
|
4
4
|
|
|
5
5
|
## Boundary
|
|
6
6
|
|
|
7
|
-
The
|
|
7
|
+
The addon owns platform process containment and launches one immutable A1 UI runtime with inherited terminal handles. It also provides native process-start inspection, terminal-mode capture and restoration, exit-notice delivery, and the bounded process-wait and process-group operations used by the exit watcher. It does not create a PTY, read ordinary terminal input, capture or parse runtime output, render, or relay terminal bytes.
|
|
8
8
|
|
|
9
|
-
On Windows it uses `windows-sys 0.61.2` to create a Job Object, enables `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`,
|
|
9
|
+
On Windows it uses `windows-sys 0.61.2` to create a Job Object, enables `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`, and creates the runtime directly in that job through `PROC_THREAD_ATTRIBUTE_JOB_LIST`. The runtime is a job member from its first instruction; the former suspended-create, assign, and resume sequence is gone. Ordinary jobs do not enable breakaway, and closing the addon-held job handle terminates remaining members.
|
|
10
10
|
|
|
11
|
-
On Linux and macOS it
|
|
11
|
+
On Linux and macOS it uses `posix_spawn` to create a dedicated process group, transfers foreground-terminal ownership only when a terminal is present, applies bounded TERM/KILL cleanup to that group, and restores the prior foreground group. Linux derives process start identity from procfs. macOS derives it from `proc_pidinfo(PROC_PIDTBSDINFO)`; PID liveness alone is never treated as identity.
|
|
12
|
+
|
|
13
|
+
## Exit watcher
|
|
14
|
+
|
|
15
|
+
After the contained runtime starts, the launch guardian starts `dist/foundation/launch-guardian/exit-watcher.js` in another Node runtime outside the guardian's containment. The watcher waits for the guardian, performs bounded Unix process-group cleanup when needed, and claims and delivers the armed exit notice with the captured terminal modes. On Windows the addon creates that Node process outside both the runtime job and the guardian's inherited job; on Unix it remains in the terminal session so terminal hang-up reaches it naturally. The guardian itself delivers the notice on ordinary completion, and claim-by-rename ensures only one owner can print it.
|
|
16
|
+
|
|
17
|
+
There is one accepted gap: a machine-wide kill aimed at every Node process, such as `taskkill /IM node.exe` or `pkill node`, kills the watcher together with the guardian and runtime. No exit notice is then printed and, on Unix, non-Node descendants left in the runtime process group are not signalled by the watcher. A1 does not work around that case by starting an A1-built executable. Terminal closure, `Ctrl+Break`, guardian failure, runtime termination, and a kill aimed only at the guardian remain covered.
|
|
12
18
|
|
|
13
19
|
## Artifact contract
|
|
14
20
|
|
|
15
|
-
Release builds place the
|
|
21
|
+
Release builds place the addon at:
|
|
16
22
|
|
|
17
23
|
```text
|
|
18
|
-
dist/native/win32-x64/process-guardian.
|
|
19
|
-
dist/native/linux-x64/process-guardian
|
|
20
|
-
dist/native/darwin-x64/process-guardian
|
|
21
|
-
dist/native/darwin-arm64/process-guardian
|
|
24
|
+
dist/native/win32-x64/process-guardian.node
|
|
25
|
+
dist/native/linux-x64/process-guardian.node
|
|
26
|
+
dist/native/darwin-x64/process-guardian.node
|
|
27
|
+
dist/native/darwin-arm64/process-guardian.node
|
|
22
28
|
```
|
|
23
29
|
|
|
24
|
-
The publication `guardians` job builds them on `windows-2025`, `ubuntu-24.04`, and `macos-15`
|
|
30
|
+
The publication `guardians` job builds them on `windows-2025`, `ubuntu-24.04`, and `macos-15` arm64; the macOS arm64 job also cross-compiles x64 with `--target x86_64-apple-darwin`. A native build is identified by loading the addon into Node and reading `version()`. When the producing runner cannot load the cross-compiled x64 addon, its manifest records only the observed Mach-O architecture.
|
|
25
31
|
|
|
26
|
-
Each
|
|
32
|
+
Each `a1-process-guardian-artifact-v2` manifest records `kind: "node-addon"`, Node-API 9, target platform and architecture, crate and protocol versions, SHA-256, byte size, build provenance, and signature/attestation status. A supported artifact is verified before loading. A missing, altered, incompatible, unsupported, or wrong-platform artifact is a concise containment launch error; A1 never silently falls back to PID-only cleanup. If a certified immutable release file disappears or changes, bootstrap restores it only from a matching installed package whose source bytes reproduce the certified size and digest, then verifies the complete release again.
|
|
27
33
|
|
|
28
34
|
## Licensing
|
|
29
35
|
|
|
@@ -4,7 +4,7 @@ Status: implemented. Milestones 1–6 deliver resident tabs as the bare-`a1` rou
|
|
|
4
4
|
|
|
5
5
|
## Context
|
|
6
6
|
|
|
7
|
-
Today bare `a1` runs as a chain: `bin/cli.js`, then a detached per-release **supervisor**, then the foreground **launch guardian**, then
|
|
7
|
+
Today bare `a1` runs as a chain: `bin/cli.js`, then a detached per-release **supervisor**, then the foreground Node **launch guardian**, then its in-process native **process-guardian addon**, then `bin/ui.js`. That last Node process hosts one in-process Pi `AgentSessionRuntime` behind the owned UI. The addon puts the UI in kill-on-close containment, so closing the terminal kills the agent; no process-guardian executable sits in the chain. `launch-instance-lifecycle` requires that behavior and reserves "a separately specified explicit resident capability" for anything that must survive. This design defines that capability.
|
|
8
8
|
|
|
9
9
|
The user set three product constraints:
|
|
10
10
|
|
|
@@ -14,12 +14,12 @@ The user set three product constraints:
|
|
|
14
14
|
|
|
15
15
|
A structured, message-based agent protocol cannot meet (2), because an extension's custom UI component is code running inside the agent process. It also forces a second, unrelated system for (3). Every tab is therefore a **terminal session**: a pseudoterminal running the complete A1 owned UI in "tab mode". Native extension components retain the declared text-terminal behavior; inline image protocols are explicitly outside the first version and use the existing text fallback.
|
|
16
16
|
|
|
17
|
-
The first version is deliberately narrower than the eventual product. It
|
|
17
|
+
The first version is deliberately narrower than the eventual product. It initially targeted Windows x64, Linux x64, and macOS arm64, with macOS x64 added by the later recorded decision; those are the targets A1 packages for the native addons. Resident tabs are the bare-`a1` route and argument-free `a1 solo` is the explicit direct-mode opt-out. Restoring tabs after reboot or logout, another architecture or platform, automatic resident-cohort upgrades, arbitrary CLI tabs, and split layouts each require a later change and their own evidence.
|
|
18
18
|
|
|
19
19
|
Existing assets:
|
|
20
20
|
|
|
21
21
|
- **`native/terminal-host`** is a working in-terminal host. It uses `libghostty-vt` pinned at `c5a21edf`, `portable-pty` 0.9.0 (ConPTY on Windows, Unix PTYs elsewhere), crossterm 0.29, and an A1-owned damage-aware frame composer. Its retained model and native encoders came from the fixed 2×2 proof; milestone 2 removed that interactive proof path and moved the model into the resident holder/attach roles. Three-platform certification remains separate from implementation.
|
|
22
|
-
- **The supervisor, process-guardian, and launch guardian** provide the patterns for detached spawn,
|
|
22
|
+
- **The supervisor, process-guardian addon, and launch guardian** provide the patterns for detached spawn, addon-backed process-start inspection, an endpoint probe with join-the-winner, and atomic metadata. The process-guardian addon has Windows, macOS, and Linux implementations.
|
|
23
23
|
- **The 2×2 spike evidence** in [`evidence/terminal-host-spike/`](evidence/terminal-host-spike/) is historical. Its verdicts stay pending and are not evidence for this design.
|
|
24
24
|
|
|
25
25
|
References:
|
|
@@ -53,8 +53,8 @@ The read-only review used A1 `develop` at `b42b3c9e`, PR #586 at `8fdb7361`, `E:
|
|
|
53
53
|
### Milestone 2 pinned launch contract (2026-10-11)
|
|
54
54
|
|
|
55
55
|
- Bare `a1` from a terminal always requests the resident tab route; with piped or redirected standard input or output it runs the direct UI without probing the host, because the attach client needs a terminal to size and own. Tab enablement is not persisted or configurable; host discovery or verification failure uses the bounded direct single-agent fallback.
|
|
56
|
-
-
|
|
57
|
-
- The host artifact resolves to `dist/native/<platform>-<arch>/
|
|
56
|
+
- The `tabsAutoName` setting lives in the flat `a1` object of the global agent `settings.json`. A stored legacy `tabsMax` value is preserved as unknown compatibility data but never limits creation or appears in Settings.
|
|
57
|
+
- The host artifact resolves to `dist/native/<platform>-<arch>/terminal-host.node`, with hermetic `A1_TERMINAL_HOST_PATH` only when `A1_DEV_INSTANCE_ID` is present; the fixed role entry is `dist/foundation/resident-tabs/host-entry.js`, and discovery may use `A1_TABS_ENDPOINT`.
|
|
58
58
|
- Tab children consume and erase `A1_TAB_ID`, `A1_TAB_TOKEN`, and `A1_TAB_ENDPOINT` before loading A1, Pi, extensions, or tools.
|
|
59
59
|
- Client-scoped bridge detach binds `clientId` and `controllerRevision` captured when the command is admitted. The additive generation-1 `attachment` and `bridge.request` fixtures freeze that shape.
|
|
60
60
|
- Launch fallback reasons are bounded to unsupported platform, no interactive terminal, missing artifact, failed probe, incompatible generation, unreadable registry, blocked recovery, unverified launch, and startup timeout. Fallback preserves resident records and presents exactly one direct-session notice.
|
|
@@ -66,10 +66,10 @@ The read-only review used A1 `develop` at `b42b3c9e`, PR #586 at `8fdb7361`, `E:
|
|
|
66
66
|
|
|
67
67
|
- The native `core/server.rs` and `core/holder.rs` state machines are deterministic and I/O-free; the production holder uses the holder core for bounded input admission and ordered writer effects, and the production server uses the server core for heartbeat supervision. Blocking identity/termination work runs through the bounded worker shell with deadlines and stale-incarnation filtering.
|
|
68
68
|
- Every attach, server, and holder role has an independent 15-second watchdog. Watchdog records are bounded, timestamped, owner-only metadata under the profile runtime directory; fault-only role freezes are refused by non-test release builds.
|
|
69
|
-
- Holders read PTY output continuously, use one bounded input authority, reserve complete pastes before delivery, and emit a typed uncertain-delivery result after an admitted write failure. Server-to-client control lanes are frame/byte bounded
|
|
69
|
+
- Holders read PTY output continuously, use one bounded input authority, reserve complete pastes before delivery, and emit a typed uncertain-delivery result after an admitted write failure. Each holder independently refreshes the server's latest complete retained surface even while hidden. Server-to-client control lanes are frame/byte bounded, render lanes retain one replaceable full current surface, and only the viewed tab is delivered to each attach client.
|
|
70
70
|
- Server replacement is serialized by `server-start.lock` in the profile runtime directory. Recovery verifies the recorded native owner before termination, waits boundedly for the registry-writer lease, increments the durable epoch, derives holder credentials, and re-admits only exact holder/child identities and incarnations. A durable three-start/sixty-second budget stops automatic replacement; explicit `--restart` clears it.
|
|
71
71
|
- Registry records now retain holder and child pid/start identities and holder incarnation. Replacement treats unverifiable records as `holder-lost`; it never starts a duplicate writer from uncertain state.
|
|
72
|
-
-
|
|
72
|
+
- Concurrent starts use an internal default of 2 and hard cap of 8. The server spaces starts by at least 250 ms and requests graceful tab shutdown before verified forced termination; open-tab count has no user-visible quota.
|
|
73
73
|
- All A1-owned Pi runtimes use one profile-neutral session-writer guard regardless of launch mode. A new target is reserved by canonical location, created as an empty owner-only placeholder, bound to stable file identity without a release gap, and only then exposed to Pi for session content. Runtime replacement acquires and binds the target before publishing it or releasing the prior writer; conflict paths preserve the source and may explicitly fork.
|
|
74
74
|
- The generation-1 holder transport continues to use the authenticated owner endpoint and replaceable full surfaces. Dedicated holder endpoints and patch deltas remain later optimizations and are not milestone-3 claims.
|
|
75
75
|
|
|
@@ -130,13 +130,13 @@ Alternatives considered:
|
|
|
130
130
|
- it duplicates a terminal core that already exists natively;
|
|
131
131
|
- v2 recorded stalls from exactly this design.
|
|
132
132
|
|
|
133
|
-
### 2. One native
|
|
133
|
+
### 2. One native addon, three Node-hosted roles, plus the A1 child
|
|
134
134
|
|
|
135
135
|
```text
|
|
136
136
|
terminal (detached, per OS user × profile)
|
|
137
137
|
┌──────────────────────────┠┌─────────────────────────────────────â”
|
|
138
|
-
│ a1 → guardian → │ │
|
|
139
|
-
│
|
|
138
|
+
│ a1 → guardian → │ │ node host-entry server │
|
|
139
|
+
│ node host-entry attach │◀───▶│ registry (atomic JSON, fsync) │
|
|
140
140
|
│ tab strip + active tab │ pipe│ topology, client fan-out, restore │
|
|
141
141
|
│ outer terminal I/O │ └───┬──────────────┬──────────────┬───┘
|
|
142
142
|
└──────────────────────────┘ │pipe │pipe │pipe
|
|
@@ -150,6 +150,8 @@ Alternatives considered:
|
|
|
150
150
|
└── tab bridge ──▶ server (status, session, name, detach)
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
Every attach, server, and holder role is `process.execPath` running the release's fixed `host-entry.js`; that entry loads the verified `terminal-host.node` addon and blocks in its native role dispatcher. Node carries no terminal bytes—the role remains native for its complete data path.
|
|
154
|
+
|
|
153
155
|
- **attach** is the foreground `a1` surface. It runs inside the bare-A1 launch instance and dies with the terminal, which is harmless.
|
|
154
156
|
- It owns raw mode, the alternate screen, outer terminal mode negotiation, the tab strip, composition of the active tab's retained surface, input arbitration, and shortcut interception.
|
|
155
157
|
- **server** is resident. It owns:
|
|
@@ -162,7 +164,7 @@ Alternatives considered:
|
|
|
162
164
|
- exactly one PTY (ConPTY on Windows) and its child process tree;
|
|
163
165
|
- one libghostty-vt retained model with bounded scrollback;
|
|
164
166
|
- input encoding and the per-tab damage state.
|
|
165
|
-
Because ConPTY belongs to its creating process, each tab has its own creator. A server crash therefore does not destroy a tab's ConPTY; holder failure affects only that tab. Actual child/tree exit and writer-lease release still require verification, not inference from the pseudoconsole closing.
|
|
167
|
+
Because ConPTY belongs to its creating process, each tab has its own creator. A server crash therefore does not destroy a tab's ConPTY; holder failure affects only that tab. Actual child/tree exit and writer-lease release still require verification, not inference from the pseudoconsole closing. The 2026-10-17 Windows x64 migration measurement put an idle Node-hosted holder at approximately 30–40 MiB of resident memory, versus a few MiB for the former native-executable holder; allocator and shared-page accounting make this a range rather than a per-tab budget. The separate holder process is retained for certified crash isolation, and latency budgets were not raised to absorb Node startup.
|
|
166
168
|
- **Why the server relays surfaces instead of clients connecting to holders directly.** It gives one authenticated endpoint per client, one ordering domain for focus and topology, and simpler multi-client arbitration. Measure the extra local pipe hop in the native hot-path evidence rather than assuming its latency. The server forwards encoded holder surfaces and does not re-render them.
|
|
167
169
|
|
|
168
170
|
### 3. Host scope, endpoints, writer authority, and credentials
|
|
@@ -182,9 +184,9 @@ Alternatives considered:
|
|
|
182
184
|
|
|
183
185
|
### 4. Detached start
|
|
184
186
|
|
|
185
|
-
- **Server start.** The server is started from the Node pre-guardian bootstrap (`ensureTerminalHost()` beside `ensureSupervisor()`), through `
|
|
187
|
+
- **Server start.** The server is started from the Node pre-guardian bootstrap (`ensureTerminalHost()` beside `ensureSupervisor()`), through `process.execPath host-entry.js server --detach` with the verified addon path supplied to the entry.
|
|
186
188
|
- **No general escape from containment.** On Windows, ordinary launch-instance jobs and holder-owned child jobs retain kill-on-close containment without `JOB_OBJECT_LIMIT_BREAKAWAY_OK` or silent breakaway; a job flag cannot allowlist an executable. On every platform, resident creation is a distinct authenticated fixed-role operation, not an arbitrary executable/argv spawn service.
|
|
187
|
-
- **Authorized launch and recovery.** Initial creation runs before the launch guardian. A contained attach client or surviving holder invokes the
|
|
189
|
+
- **Authorized launch and recovery.** Initial creation runs before the launch guardian. A contained attach client or surviving holder invokes the addon's verified `server --detach` recovery path; there is no arbitrary-spawn IPC method. When `runRole` first enters, it records one immutable host command: the current Node executable and the release's fixed role entry. The native routine accepts only a typed `server` or `holder` role and canonical profile, opens its own addon artifact, constructs the fixed role grammar, uses an allowlisted environment/cwd with handle inheritance disabled, and starts that recorded Node-and-entry pair. It verifies the resulting pid/start identity and containment before success. On Windows, a contained caller uses the fixed WMI escape, while the holder's fixed recovery invocation launches directly from its already-detached resident boundary; recursively invoking WMI from a detached service can fail in non-interactive sessions. Native tests may compile one feature-gated fixture role, but production accepts no caller-supplied executable or free-form argv. Owner-only credentials prevent cross-user access; this is not a sandbox against arbitrary malicious code already running as the same OS user.
|
|
188
190
|
- **Windows:**
|
|
189
191
|
- Select valid platform creation flags for detached, console-free startup; do not assume combining `DETACHED_PROCESS` and `CREATE_NO_WINDOW` adds a guarantee. Verify resulting process identity and job containment before reporting resident readiness.
|
|
190
192
|
- For a caller in a foreign kill-on-close job that denies breakaway (Windows OpenSSH, some IDE terminals), use WMI `Win32_Process.Create` with an explicit environment block and cwd, bounded startup, and post-launch verification (herdr reference).
|
|
@@ -192,7 +194,7 @@ Alternatives considered:
|
|
|
192
194
|
- **Every platform** records the observed detachment mode and any failed verification reason.
|
|
193
195
|
- **macOS and Linux:**
|
|
194
196
|
- Start resident roles in a new session (`setsid`) with no controlling terminal, standard streams on `/dev/null`, and an explicit environment and cwd, so terminal close and SSH hang-up (`SIGHUP`) do not reach them. Verify the resulting session, process group, and identity before reporting readiness.
|
|
195
|
-
- The process-guardian's kill-on-close tree tracking must not adopt resident roles, and ordinary descendants must not gain a way to leave their tree.
|
|
197
|
+
- The process-guardian addon's kill-on-close tree tracking must not adopt resident roles, and ordinary descendants must not gain a way to leave their tree.
|
|
196
198
|
- macOS: keep the resident roles in the user's per-user bootstrap namespace, so clipboard, keychain, and other per-user services keep working after the launching terminal closes (herdr #4100 is the reference). Linux: a session manager that kills user processes at logout (systemd `KillUserProcesses`) ends tabs at logout; that is the documented logout behavior, not a failure.
|
|
197
199
|
- Milestone 1 proves these with fixtures on each platform before any milestone relies on survival.
|
|
198
200
|
- **Holders** are spawned by the server through the same platform detach routine.
|
|
@@ -221,13 +223,13 @@ The channels carry bounded generation-1 frames: a u32 little-endian length follo
|
|
|
221
223
|
- The server sends `bridge.visibility{visible}`, so a hidden A1 throttles animation, and `bridge.rename`, so a strip rename sets the Pi session name.
|
|
222
224
|
- The bridge is advisory. A missing or broken bridge degrades that tab to process-level status and never breaks the tab.
|
|
223
225
|
- **Backpressure:**
|
|
224
|
-
- Each client connection has a reliable, bounded control lane and a **single-slot render lane**. A newer full current surface replaces an unsent one, so a client that drains never depends on a dropped intermediate revision.
|
|
225
|
-
- The server never blocks a holder, another client, or registry writes on a slow client.
|
|
226
|
+
- Each client connection has a reliable, bounded control lane and a **single-slot render lane**. A newer full current surface replaces an unsent one, so a client that drains never depends on a dropped intermediate revision. Structured surfaces are immutable `Arc` values while routed; encoding happens outside the routing mutex, decoded structured state is capped at 8 MiB, and a superseded holder socket cannot publish into a newer incarnation.
|
|
227
|
+
- The server never blocks a holder, another client, or registry writes on a slow client. Attach and holder hot loops yield after bounded frame, byte, and two-millisecond work quanta; completed outer writes, rather than offers, advance the presentation baseline.
|
|
226
228
|
- Holders parse their PTY continuously, whether or not the tab is viewed, so a PTY is never throttled and a child never blocks on output.
|
|
227
229
|
|
|
228
230
|
### 6. Rendering, input, and outer-terminal fidelity
|
|
229
231
|
|
|
230
|
-
- **Composition.** The attach client composes row 0 (the strip) and rows `1..N` (the viewed tab's surface). The tab PTY is sized `cols × (rows − 1)`.
|
|
232
|
+
- **Composition.** The attach client composes row 0 (the strip) and rows `1..N` (the viewed tab's surface). The tab PTY is sized `cols × (rows − 1)`. Ghostty remains the sole child-terminal parser. Its stream parser reports synchronized-output begin/end/reset boundaries to the holder, which keeps mutable parser state separate from an immutable committed surface and never publishes an open transaction. A non-renewable 250 ms deadline publishes one typed recovery commit for a malformed transaction. The outer composer owns the only synchronized envelope and falls back to a full repaint only when identity, geometry generation, dimensions, or the structured base changes.
|
|
231
233
|
- **Outer modes.** The client negotiates the outer terminal's modes: raw mode, alternate screen, bracketed paste, focus events, SGR mouse, kitty keyboard protocol where available, and synchronized output.
|
|
232
234
|
- Child-requested modes are tracked in the holder's model. Input is re-encoded per child mode by libghostty-vt, which the proof already does.
|
|
233
235
|
- Mouse rows are offset by the strip. Row 0 belongs to the client.
|
|
@@ -247,8 +249,8 @@ Direct `a1 solo` is the behavioural oracle for every viewed resident child. Only
|
|
|
247
249
|
- **Typed effects.** The holder registers libghostty's clipboard-write callback, which normalizes OSC 52 and OSC 1337. A write from the viewed tab crosses as one bounded `terminal.effect`; attach writes one OSC 52 for it. A hidden tab's write is dropped, never replayed into a later view. Payloads are never logged.
|
|
248
250
|
- **Modes and cursor.** Each surface carries the child's cursor cell and modes. Attach mirrors a cursor shape the child chose and resets it on detach or failure; a default block cursor is left to the outer terminal, as direct A1 leaves it.
|
|
249
251
|
- **Shortcut projection.** The owned child projects the effective A1 keybindings plus `keybindings.json` into one assignment per tab action: a key an A1 action holds never becomes a tab shortcut, and the earlier declaration keeps a duplicated key. It publishes the projection as `bridge.shortcuts`; the server relays it as `tabs.shortcuts`, and attach dispatches and labels exactly that projection. `/hotkeys` renders the same projection. A shared case file is replayed by both resolvers.
|
|
250
|
-
- **Committed presentation.** A requested view commits only with a topology that lists it and a complete target surface. Surfaces of the committed view apply in revision order, and revisions rise across holder incarnations. A retained re-offer is marked stale and parks for at most 120 ms while the fresh frame arrives. One compositor writes strip, body, overlay, and final cursor inside a single synchronized update
|
|
251
|
-
- **Evidence.** One interaction matrix drives three layers: an owned-shell oracle that records content-free observations, role fixtures that drive real server, holder, and attach processes with a certified parity child, and
|
|
252
|
+
- **Committed presentation.** A requested view commits only with a topology that lists it and a complete target surface. Retained rows, palette/default colors, modes, and cursor are captured atomically; equal captures do not invent a revision. Surfaces of the committed view apply in revision order, and revisions rise across holder incarnations. A retained re-offer is marked stale from the request start and parks for at most 120 ms while the fresh frame arrives. One compositor writes strip, body, overlay, modes, and the viewport-clipped final cursor inside a single synchronized update. It erases suffix cells when a complete replacement row becomes shorter and restores the newest underlying surface when an overlay leaves. Clock, hover, and title-animation damage repaints only the strip and restores the child's cursor.
|
|
253
|
+
- **Evidence.** One interaction matrix drives three layers: an owned-shell oracle that records content-free observations, role fixtures that drive real server, holder, and attach processes with a certified parity child, and exact-artifact harnesses. `verify-interaction-parity.mjs` compares the bytes the same child observes directly and through the built host. `verify-resident-parallel-progress.mjs` configures the real engine against a bounded loopback streaming provider and crosses all six first-response orders with all three initial views while every turn remains unfinished behind a completion barrier. At matching 100×29 child geometry, the same real-A1 prompt also runs through isolated direct-solo and resident profiles; the native Ghostty oracle compares a content-free digest of the intermediate fixture cells and their resolved styles together with normalized cursor and mode state. It advances only at parser-reported synchronized-output boundaries, rejects unbalanced transactions or disappearing intermediate cells, and excludes only the resident strip row rather than normalizing terminal semantics. Opt-in per-process JSONL observations identify engine delivery, completed child writes, PTY ingestion, distinct holder commits, attach receipt, and completed outer paint by run/tab/incarnation/process-instance/sequence without recording terminal or conversation content; queue loss, clock faults, duplicates, stale order, and missing stages fail the report. Named pressure variants keep all turns unfinished while exercising hidden progress, repeated switching, navigation input, rename overlays, tab-local resize, a bounded 35 ms attach-receipt delay, and a 900 ms paused outer sink; every tab must retain partial animation/progress before settlement and then present its final output. A detached/reconnect variant releases all three unfinished turns after terminating the authenticated attach and exact self-reported server identity; child writes, PTY ingestion, and holder commits must continue before a replacement server and attach present every retained final output. Separate isolated exact-artifact control markers stop one target tab at engine delivery, physical child rendering, holder commit, or outer paint; each control requires its upstream stages and the other tabs to advance, then removes the stop and requires final output from all three tabs. A fifth control fragments a real owned-shell synchronized write into at most 64 physical writes without changing its bytes, records the DEC private mode 2026 transaction classification, and requires ordinary complete holder commits without deadline recovery before final output. This characterizes the built PTY/ConPTY coherence path; a passing fragmented run does not by itself assign the historical stall to that path without a failing live reproduction. The marker path is absent in ordinary sessions and is scrubbed before extensions or tools inherit the child environment, while observation callbacks remain passive and cannot select a control. CI runs the vendored Ghostty VT tests and both exact-artifact harnesses on Windows x64, Linux x64, and macOS arm64. Each platform's verdict stands alone.
|
|
252
254
|
|
|
253
255
|
### 7. Status and attention come from the tab bridge
|
|
254
256
|
|
|
@@ -289,7 +291,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
289
291
|
- restarts: `restarts`, `restartWindowStart`, `lastExit`
|
|
290
292
|
- `registryRevision`, `epoch`, and `bootId`
|
|
291
293
|
- **No per-tab credential is persisted in the registry.** The owner-only profile secret is stored separately and derives per-tab/per-incarnation credentials from recorded non-secret identities. The environment sent with `tab.create` is kept in holder memory only; a tab restarted after a crash reuses its holder's environment.
|
|
292
|
-
- **Session-writer lease.** All A1-owned session writers (resident, solo/fallback, and Pi-comparison) on the supported platforms use one profile-neutral native lock primitive before opening a session for writing, independent of launch mode. It does not start a host, holder, or bridge for direct modes. The primitive is an OS lock held by the writer process itself (`LockFileEx` on Windows, an open-file-description `fcntl` lock on Linux, `flock` on macOS), so the kernel releases it only when the writer is gone. Node writers take it in-process through
|
|
294
|
+
- **Session-writer lease.** All A1-owned session writers (resident, solo/fallback, and Pi-comparison) on the supported platforms use one profile-neutral native lock primitive before opening a session for writing, independent of launch mode. It does not start a host, holder, or bridge for direct modes. The primitive is an OS lock held by the writer process itself (`LockFileEx` on Windows, an open-file-description `fcntl` lock on Linux, `flock` on macOS), so the kernel releases it only when the writer is gone. Node writers take it in-process through the same Node-API 9 `terminal-host.node` addon that exports the resident roles; there is no separate lock artifact, and a helper process cannot hold a lock on their behalf. Existing files are opened, identified by stable volume/device plus file identity, locked through that identity, and revalidated so case, junction/symlink, and hard-link aliases converge. A new file first holds a canonical-parent/name reservation, creates/opens without writing session content, acquires and revalidates file-identity authority while the reservation remains held, and only then admits writing; losing that race writes nothing. The writer routes include startup, `--session`, in-runtime switches, fork, import, new session, and the picker rename. The registry and holder coordinate admission, but a holder-only lock is insufficient: native lease custody must remain valid for the actual writer's lifetime, including any interval after holder death. Pin and prove the writer-bound lock/handle design on each platform in milestone 1; do not infer child death from pseudoterminal loss. Replacement requires lease acquisition and verified prior-writer/tree exit; uncertainty blocks it. Session switching acquires the target before relinquishing the old writer lease, and failed switches preserve the original session. Resident selection focuses an existing tab; direct-mode conflicts fail safely or offer an explicit fork, never silently attach or overwrite. Unmodified external Pi, older nonparticipating builds, and arbitrary file writers are outside this cooperative guarantee; document that limitation rather than claiming an OS sandbox.
|
|
293
295
|
- **Prompt journal.** `<dataDir>/tabs/<profile-token>/<tabId>/journal.jsonl` is an owner-only child-owned recovery journal, separate from asynchronous prompt history. Each submission has a stable ID and session/incarnation identity. Admission durably records bounded identity and recoverable editor text, never duplicate attachment payload bytes, before dispatch; timeout/failure leaves the prompt in the editor with a visible failure and dispatches nothing. The actual engine command retains every validated attachment. Correlate the record with the committed session entry, and retire it only after the corresponding session data is synchronized, including when command completion observes that the lifecycle already returned to ready; a settled turn's durable completion also records its submission ID so recovery never offers a completed prompt as unfinished. Recovery merges journal/session evidence by identity and never automatically resends, including when a crash occurs between dispatch, session append, sync, and journal retirement. It rehydrates image chips only from validated profile sidecars and removes unavailable tokens rather than presenting inert attachment labels. Preserve the first-turn journal and reserved session identity if Pi has not created its file yet; distinguish that from an unexpectedly missing existing transcript.
|
|
294
296
|
- **Draft checkpoint.** Use a periodic dirty checkpoint with a maximum one-second dirty interval while storage is healthy, including uninterrupted typing, rather than inactivity-only debounce. Expose failed/late checkpoints as degraded recovery; never continue claiming the one-second bound during a blocked disk or event-loop stall. Drafts and submitted prompts have different guarantees.
|
|
295
297
|
|
|
@@ -312,7 +314,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
312
314
|
|
|
313
315
|
### 10. Updates and version skew
|
|
314
316
|
|
|
315
|
-
- **
|
|
317
|
+
- **Artifacts and process images.** The server and holders are Node runtimes that load the addon and fixed entry from immutable release directories, never from the mutable npm prefix. Activation therefore never overwrites a loaded addon or entry, and release retention keeps every release used by a verified resident process.
|
|
316
318
|
- **Same generation, newer client.** The client attaches normally, and features the server lacks are disabled per method.
|
|
317
319
|
- **No automatic handoff in the first version.** A compatible resident cohort remains on its immutable release until all of its tabs stop. A newer same-generation client may attach with unsupported optional operations disabled.
|
|
318
320
|
- **Retention.** Release collection keeps the server's, every holder's, and every tab child's release while native identities verify. Package activation does not terminate or overwrite them.
|
|
@@ -322,8 +324,8 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
322
324
|
|
|
323
325
|
- **Strip.** Row 0, drawn by the attach client, reproduces the approved v2 default appearance:
|
|
324
326
|
- normal chips use RGB background `38,38,38` with foreground `170,170,170`, hover uses `48,48,48`, and the viewed chip uses `55,55,55` with white text, plus v2's palette fallback;
|
|
325
|
-
- the empty state
|
|
326
|
-
- chips are at most 20 columns, clipped with `…` by grapheme width, with no separators
|
|
327
|
+
- the empty state contains only the hoverable `+` control, with no clock, wallpaper, hint, or child cursor;
|
|
328
|
+
- chips are at most 20 columns, clipped with `…` by grapheme width, with no separators or close glyph;
|
|
327
329
|
- a `…` overflow menu holds tabs that do not fit, and `+` is always reserved;
|
|
328
330
|
- these presentation choices are fixed, not settings, while each working spinner uses that tab's reported A1 accent.
|
|
329
331
|
- **Keys.** These are intercepted by the attach client before input encoding. They are configurable through a keybindings file and conflict-checked against A1's registry at launch.
|
|
@@ -333,6 +335,7 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
333
335
|
| `Alt+A` | new tab |
|
|
334
336
|
| `Alt+W` | close |
|
|
335
337
|
| `F2` | rename |
|
|
338
|
+
| `Alt+.` | create or activate the global Settings tab |
|
|
336
339
|
| `Alt+1`…`Alt+9`, `Alt+0` | jump to a tab |
|
|
337
340
|
| unbound (`tabs.next` / `tabs.previous`) | next / previous, wrapping |
|
|
338
341
|
| unbound (`tabs.moveRight` / `tabs.moveLeft`) | move the tab right / left |
|
|
@@ -343,43 +346,43 @@ Tab status is an A1-owned state machine fed by `bridge.status`:
|
|
|
343
346
|
- **`Ctrl+C` twice leaves, in every tab (user decision).** The client forwards the first `Ctrl+C` to the tab unchanged, so a single press still clears, copies, or interrupts. It consumes the second press within the existing clear/exit interval as the detach request and does not forward it. The tab therefore never receives a second `Ctrl+C` from the chord, which removes v2's bug where `Ctrl+C+C` killed the agent. The trade-off: a future CLI tab cannot receive two quick `Ctrl+C`s. Pressing them more slowly than the interval still reaches the program.
|
|
344
347
|
- **Mouse on row 0:**
|
|
345
348
|
- click activates a tab;
|
|
346
|
-
-
|
|
347
|
-
- right-click opens `Rename`/`Close`;
|
|
349
|
+
- right-click opens `Rename`/`Close` (`Close` only for Settings);
|
|
348
350
|
- dragging reorders, with a `│` drop marker, after 250 ms or once the pointer moves;
|
|
349
|
-
- `+`
|
|
351
|
+
- `+` opens a persistent dark v2 menu with `+ Agent`, a separator, and `Settings`; hover and click remain active through the opening release.
|
|
352
|
+
- **Settings.** The strip owns one profile-global `Settings` tab. The grouped launcher and `Alt+.` create or activate it; closing the screen or tab removes it. Resident Agent autocomplete omits `/settings`, and manually entering it does not open a per-Agent screen. `a1 solo` and `a1 pi` retain their existing Settings routes.
|
|
350
353
|
- **Commands inside A1 tabs:** existing `/name <name>` also renames the tab. Milestone 5 adds no tab-management slash commands: `/new-tab`, `/close`, `/tabs`, `/quit-all`, and `/restart-tab` are not exposed.
|
|
351
354
|
- **Naming:**
|
|
352
355
|
- Default names are `agent` or `agent N`.
|
|
353
356
|
- When `tabsAutoName` is on (the default, by user decision), the tab's model proposes a hyphenated lowercase name of at most 16 characters after the first settled turn. It uses a small budget and one retry, with a fallback derived from the first prompt, and it runs inside the child.
|
|
354
357
|
- A user rename sets `nameSource=user`, and auto-naming never overrides it.
|
|
355
358
|
- An automatic rename uses v2's title transition: 600 ms moving rainbow, typing at 45 ms per character, then a 300 ms settle. Manual renames do not animate.
|
|
356
|
-
- Rename is inline in the chip: `Enter` commits,
|
|
357
|
-
- **Close.** `Alt+W
|
|
359
|
+
- Rename is inline in the chip: `Enter` commits, `Esc` or a click elsewhere cancels, and `Ctrl+Left` / `Ctrl+Right` move by word. The name is trimmed, must not be empty, is at most 64 characters, and has control characters stripped.
|
|
360
|
+
- **Close.** `Alt+W` and the right-click menu close a tab directly, including while it is working. Closing removes the chip without a confirmation, stopping/read-only frame, or close glyph, then gracefully stops the child under the existing bounded termination policy. Its session stays resumable. Closing the last tab leaves only the hoverable `+` control.
|
|
358
361
|
- **Detach.**
|
|
359
362
|
- `Ctrl+C` twice detaches from any tab and is handled by the attach client, so it works even when the bridge is down. Inside an A1 tab, `/quit` and empty-editor `Ctrl+D` send a detach request carrying the immutable command-origin controller identity and revision; stale, ambiguous or unavailable attribution keeps the tab running and directs the user to `Ctrl+C` twice.
|
|
360
363
|
- The client restores the terminal. When agents are still running, it prints `1 agent still running run a1 to return.` or `N agents still running run a1 to return.`; otherwise it prints the resume hint.
|
|
361
364
|
- **Launch and reattach:**
|
|
362
365
|
- Bare `a1` ensures the server and attaches.
|
|
363
366
|
- With zero tabs, it creates one A1 tab in the launch cwd.
|
|
364
|
-
- Otherwise it views the last active tab from its retained surface without byte-history replay. Measure retained first paint and first input separately from bootstrap; status for the remaining tabs arrives without subscribing to their surfaces.
|
|
367
|
+
- Otherwise it views the last active tab from its retained surface without byte-history replay. Every holder keeps draining its PTY and updating its retained terminal independently while hidden; selecting a working tab requests its newest complete partial-response surface immediately and then continues live delivery. Measure retained first paint and first input separately from bootstrap; status for the remaining tabs arrives without subscribing to their surfaces.
|
|
365
368
|
- `a1 --session X` opens X in a new tab, or focuses the tab holding it.
|
|
366
369
|
- A new tab's cwd is the client's launch cwd.
|
|
367
|
-
- One internal prewarmed standby keeps the startup graph loaded in a hidden A1 process after a trusted tab has reported its session. Before promotion it owns no session or writer lease
|
|
370
|
+
- One internal prewarmed standby keeps the startup graph loaded in a hidden A1 process after a trusted tab has reported its session. Before promotion it owns no session or writer lease and has no registry/tab-strip entry. Creation promotes it only for an exact cwd, release, Node executable, and environment match; the server durably commits the new tab before exposing it to input. A mismatch discards it and normal creation retargets the next standby from the newly trusted tab. Standby launch or loss never changes visible tab state. It is not a setting. Measure A1 warm promotion directly; v2's tens-of-milliseconds result is reference evidence only.
|
|
368
371
|
- **Several terminals.** Each client has its own active tab, but each tab has one explicit input controller. Focus gain automatically claims the viewed tab through Decision 5's causal input barrier; if focus reporting is unavailable, first input starts the claim and is held until acknowledgement. There is no `Enter` takeover or persistent view-only banner. Only the current controller may send input or set PTY size. Delayed `/quit` and empty-editor `Ctrl+D` preserve their submission origin; stale or unprovable attribution affects neither client and falls back to attach-local double-`Ctrl+C`.
|
|
369
372
|
- **Background attention.** Only the chip icon changes. There is no bell, sound, or notification, and no setting for one.
|
|
370
373
|
|
|
371
374
|
### 12. Limits and resources
|
|
372
375
|
|
|
373
|
-
|
|
376
|
+
Internal resources remain bounded without imposing a user-visible tab-count quota.
|
|
374
377
|
|
|
375
|
-
-
|
|
378
|
+
- The only user-facing Tabs setting during resident launches is `tabsAutoName` (default on). Legacy `tabsMax` data is preserved but ignored. Tab enablement is not a setting. Direct `a1 solo` and `a1 pi` launches omit the Tabs section entirely.
|
|
376
379
|
- Unresponsive restart and stall-notice timing are internal fixed behavior at 120 and 300 seconds; concurrent starts use internal default 2/hard cap 8 and prewarm uses internal count/hard cap 1.
|
|
377
380
|
- Every open tab keeps its holder and A1 process until explicit close. There is no idle-suspension setting, timer, lifecycle, or glyph.
|
|
378
381
|
- Scrollback is capped at 10 MiB per holder, and the replay after a server restart is the retained model itself.
|
|
379
382
|
- The server exits after 10 minutes with no attached client and no live user tab; the prewarmed standby does not keep it alive and is stopped first.
|
|
380
383
|
- Structured diagnostic logs are `terminal-host-server.log` and `terminal-host-holder-<tab>.log`, rotated at 5 MiB with three kept. Use allowlisted fields/reason codes, not arbitrary exception strings, prompt text, terminal content, or credentials.
|
|
381
384
|
- Raw child stderr cannot be declared sanitized: when available separately from ConPTY's combined terminal stream, store it only under the owner-only per-tab recovery area, at 5 MiB with three generations and a seven-day maximum age, alongside bounded last-screen files. Do not scrape stderr back out of terminal cells. These private artifacts may contain conversation or sensitive content and are excluded from `a1 tabs doctor`; neither their contents nor arbitrary extension errors enter structured crash records.
|
|
382
|
-
- `a1 tabs host status` prints the server pid, build, release, generation, epoch, uptime, detachment mode, tab count by status, client count,
|
|
385
|
+
- `a1 tabs host status` prints the server pid, build, release, generation, epoch, uptime, detachment mode, tab count by status, client count, internal resource limits, volatile health counters since server start, and log paths.
|
|
383
386
|
|
|
384
387
|
#### Hard bounds (milestone 6 audit)
|
|
385
388
|
|
|
@@ -388,12 +391,13 @@ Every queue, buffer, file, and collection the resident roles keep has a declared
|
|
|
388
391
|
| Bound | Cap | Enforced in | Test |
|
|
389
392
|
|---|---|---|---|
|
|
390
393
|
| Protocol frame | 2 MiB, nesting 64, 65,536 collection items | `native/terminal-host/src/protocol.rs`, `src/foundation/resident-tabs/protocol.ts` | `refuses_oversized_header_before_payload`; `refuses a frame header past 2 MiB before buffering its payload`, `refuses nesting past 64 levels`, `refuses a collection past 65,536 items` |
|
|
394
|
+
| Structured complete surface | 512 × 256 geometry, 65,536 cells, 4,096 styles, 4,096 links, and 8 MiB decoded state | `protocol.rs` `StructuredSurface::validate` | `structured_surfaces_fail_closed_on_dimensions_indexes_and_control_injection` |
|
|
391
395
|
| Input per message and holder input queue | 1 MiB | `protocol.rs`, `core/holder.rs`, `roles/holder.rs` | `over_capacity_paste_delivers_no_prefix` |
|
|
396
|
+
| Holder PTY ingress and work round | 64 × 16 KiB lossless chunks; at most 256 KiB or 2 ms before yielding | `roles/holder.rs` | `verify-resident-parallel-progress.mjs` concurrent and stopped-stage cases |
|
|
392
397
|
| Per-client control lane | 1,024 frames and 4 MiB | `roles/server.rs` | `control_lane_is_bounded_by_frames_and_bytes` |
|
|
393
398
|
| Per-client render slot | one pending full surface; a newer one replaces it | `roles/server.rs` `Peer::offer_surface` | `stalled_client_gets_one_full_current_surface` |
|
|
394
399
|
| Concurrent endpoint connections | 128 | `roles/server.rs` `admit_connection` | `connections_beyond_the_cap_are_refused_and_released_slots_are_reused` |
|
|
395
400
|
| Attached viewing clients | 16 | `roles/server.rs` `client_slot_available` | `attached_clients_are_capped` |
|
|
396
|
-
| Open tabs | `tabsMax` (default 10, hard 50), including session-selection starts | `roles/server.rs` `tab.create`, `attach.start` | `ten_concurrent_tab_starts_respect_the_server_limit` |
|
|
397
401
|
| Concurrent holder starts | default 2, hard 8 | `core/server.rs` | `limits_starts_and_drops_stale_completion` |
|
|
398
402
|
| Stopped tab records | 16, oldest dropped first | `roles/server.rs` `prune_stopped_records` | `stopped_records_are_bounded_oldest_first_and_live_tabs_are_kept` |
|
|
399
403
|
| Prior tab-set archives | bounded history | `roles/registry.rs` | `prior_tab_sets_are_bounded` |
|
|
@@ -405,12 +409,14 @@ Every queue, buffer, file, and collection the resident roles keep has a declared
|
|
|
405
409
|
| Session reconciliation read | 64 MiB | `src/foundation/resident-tabs/recovery.ts` | `refuses to scan a session past the 64 MiB reconciliation cap` |
|
|
406
410
|
| Release-hold report | 32 uncertainty reasons; 1 MiB read | `roles/release_holds.rs`, `release-holds.ts`, `bounded-host-command.ts` | `uncertainty_reasons_stay_within_their_cap`; `fails a release-hold report that pushes past its byte cap without returning a prefix` |
|
|
407
411
|
| Maintenance exchange | 5 s reads, 60 s stops, 4 MiB report | `roles/maintain.rs`, `maintenance.ts`, `bounded-host-command.ts` | `fails a maintenance report that pushes past its byte cap without returning a prefix`, `kills and rejects a command that outlives its deadline` |
|
|
408
|
-
| Attach
|
|
412
|
+
| Attach server ingress | reader queue 64 frames/4 MiB; execution queue 8 frames/4 MiB; at most 64 KiB or 2 ms before yielding | `roles/attach.rs` | `verify-resident-parallel-progress.mjs` slow-receipt and switching cases |
|
|
413
|
+
| Attach input | 1,024 parsed events; semantic input is emitted in at most 1 MiB chunks and target-bound while a view/create is pending | `roles/attach.rs` | `deferred_input_is_atomic_bounded_and_bound_to_the_requested_tab` |
|
|
414
|
+
| Outer presentation writer | one replaceable complete desired state, 8 control/effect frames, and 4 MiB effect bytes | `roles/attach.rs` `OuterWriter` | `presentation_is_one_ordered_synchronized_write`; `verify-resident-parallel-progress.mjs` paused-sink case |
|
|
409
415
|
| Doctor bundle | 25 MiB, oldest log generations dropped first | `src/foundation/resident-tabs/doctor.ts` | `drops the oldest log generations first to stay within its cap` |
|
|
410
416
|
|
|
411
417
|
### 13. Packaging and certification
|
|
412
418
|
|
|
413
|
-
- `
|
|
419
|
+
- `terminal-host.node` and the fixed `dist/foundation/resident-tabs/host-entry.js` are built and packaged for Windows x64, Linux x64, macOS arm64, and macOS x64 (see the 2026-10-14 decision), extending the impact-selected Windows CI owner introduced by #588 to that exact target matrix, with Zig 0.15.2 for pinned libghostty-vt. One addon carries both every role and the session-writer lock; there is no terminal-host executable or separate lock addon. It ships in the immutable release with the v2 addon manifest, hash verification, provenance, licenses, and notices.
|
|
414
420
|
- Acceptance requires exact-package hermetic suites on each platform plus manual or isolated-worker evidence per platform for terminal close, SSH/session loss, reattach, input fidelity, extension text UI, failure recovery, rollback, and render smoothness. Evidence is never inferred from one platform to another.
|
|
415
421
|
- An authorized isolated-worker 24-hour soak remains required certification evidence on each platform; a miss blocks certification without silently changing the approved default or weakening fallback.
|
|
416
422
|
|
|
@@ -498,9 +504,9 @@ The v2 daemon was unreliable for structural reasons, and a forensic pass over it
|
|
|
498
504
|
2. With `TERMINAL_HOST_LOCAL_BUILD=1` and `TERMINAL_HOST_PREBUILT_VT` naming that directory, `build.rs` verifies `terminal-vt.manifest.json` against the vendored `VERSION`, the Ghostty commit, the Cargo and Zig targets, and each file's SHA-256, then links it and skips Zig. A mismatch fails the build in one line and never falls back. Builds with `CI` set ignore the variable.
|
|
499
505
|
3. Run one test: `cargo test --manifest-path native/terminal-host/Cargo.toml --features resident-fixtures --test role_integration <name>`.
|
|
500
506
|
|
|
501
|
-
Every chaos run prints its seed and records its executed action schedule in its report; `--seed <n>` fixes the seed, and `--replay <report.json>` re-executes a recorded schedule and reports actions it could not apply as divergence.
|
|
507
|
+
Every chaos run prints its seed and records its executed action schedule in its report; `--seed <n>` fixes the seed, and `--replay <report.json>` re-executes a recorded schedule and reports actions it could not apply as divergence. With the verified prebuilt terminal library selected, `--build` builds the release addon and the feature-gated `fake-tab` fixture; otherwise pass `--host <terminal-host.node>` and `--fake-tab <fixture>` explicitly. Product builds never emit a role executable; the `role-host` and `fake-tab` binaries are test fixtures only.
|
|
502
508
|
|
|
503
|
-
Known limit (2026-10-15): on a Windows desktop whose shells already run inside a job, a server started by a role test cannot leave that job, `verify_detached` rejects it, and tests that expect `started` fail locally while passing in CI; the release retention integration test, which runs whenever debug host
|
|
509
|
+
Known limit (2026-10-15): on a Windows desktop whose shells already run inside a job, a server started by a role test cannot leave that job, `verify_detached` rejects it, and tests that expect `started` fail locally while passing in CI; the release retention integration test, which runs whenever a debug host addon exists, fails the same way, and local chaos reports servers that holders did not replace. A separate change addresses this.
|
|
504
510
|
|
|
505
511
|
**Milestone 5 preview performance budgets (p95 on each exact platform build):** cold launch to first input is the direct startup budget plus 800 ms; matching-standby creation to first input is 250 ms; reattach first paint is 300 ms and first accepted input is 400 ms; input-to-process is 16 ms; output-to-present is 33 ms. Each result or miss is recorded independently for Windows x64, macOS arm64, and Linux x64. The platform-native validation lanes run `measure-resident-tabs.mjs` against their built host: its instrumented child reports process-admission timestamps through a real resident holder, while the outer test PTY records presentation, standby promotion, and retained-surface reattach timestamps.
|
|
506
512
|
|
|
@@ -508,8 +514,8 @@ Known limit (2026-10-15): on a Windows desktop whose shells already run inside a
|
|
|
508
514
|
|
|
509
515
|
- **[A server crash interrupts the display for everyone]** → Holders keep processes and screens alive. The server holds no VT, Pi, or extension code. It is auto-respawned by any client or holder, and reattach restores exact surfaces.
|
|
510
516
|
- **[Terminal fidelity in a composed surface]** (keyboard protocols, mouse, paste, IME, Unicode width, hyperlinks) → Reuse libghostty-vt encoding and the proof's input work, and add fidelity suites on each platform. Claims cover only the certified text-terminal contract; image protocols remain an explicit gap.
|
|
511
|
-
- **[Per-tab memory multiplies with holder and A1 processes;
|
|
512
|
-
- **[Detachment is platform-specific and fragile]** (Windows jobs and SSH, macOS bootstrap namespaces, Linux logout policies) → Detection and escape live in the native
|
|
517
|
+
- **[Per-tab memory multiplies with holder and A1 processes; measured working sets are observations, not A1 bounds]** → The 2026-10-17 Windows x64 measurement shows Node hosting raising an idle holder from a few MiB to approximately 30–40 MiB resident. One internal prewarmed standby, bounded concurrent starts, and throttled hidden delivery limit avoidable multiplication. Every open tab keeps its process until explicit close; there is no idle suspension or count quota. Merging holders into the server would discard the certified per-tab crash boundary and requires a separate change.
|
|
518
|
+
- **[Detachment is platform-specific and fragile]** (Windows jobs and SSH, macOS bootstrap namespaces, Linux logout policies) → Detection and escape live in the native addon per platform, while the process image is Node running the fixed role entry. The mode is reported, never assumed, and unverifiable survival falls back instead of claiming persistence.
|
|
513
519
|
- **[Three platforms multiply native and certification work]** → Platform code is confined to the thin I/O shell around one shared sans-IO core, and milestone 1 proves every platform primitive before later milestones build on it.
|
|
514
520
|
- **[The Zig and libghostty toolchain in CI and release]** → It is pinned already. Extend the #588 Windows owner and provenance gate to a macOS and Linux build matrix.
|
|
515
521
|
- **[No image protocol in tabs at first]** → Pi's text fallback applies and the gap is documented. Forwarding is a follow-up.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Terminal-host provenance
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The resident terminal host uses a small console-hosted stack. Its native implementation ships as the Node-API 9 addon `terminal-host.node`; the release's fixed `dist/foundation/resident-tabs/host-entry.js` loads that addon and dispatches the server, holder, attach, probe, maintenance, and internal roles inside the hosting Node runtime. It runs inside the user's existing terminal and does not create a separate desktop window. No terminal-host executable is built or shipped, and the same addon exports the in-process session-writer lock.
|
|
4
4
|
|
|
5
5
|
## Selected source components
|
|
6
6
|
|
|
@@ -14,7 +14,7 @@ The composed-terminal proof uses a small console-hosted stack. It runs inside th
|
|
|
14
14
|
|
|
15
15
|
## Release notices
|
|
16
16
|
|
|
17
|
-
`scripts/release/generate-terminal-host-notices.mjs` writes `terminal-host-THIRD-PARTY-NOTICES.txt` beside each release
|
|
17
|
+
`scripts/release/generate-terminal-host-notices.mjs` writes `terminal-host-THIRD-PARTY-NOTICES.txt` beside each release addon. It walks the locked Cargo graph for the build target (normal dependencies, including the Node-API glue; proc-macro crates and their subtrees never reach the addon), adds the Zig-linked components, and fails on a license expression outside MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, Unicode-3.0, or Zlib (the `LLVM-exception` addendum is accepted), on a crate without license text, or on a pinned text whose digest changed.
|
|
18
18
|
|
|
19
19
|
A buffered A1-owned frame composer is the initial presentation layer. Ratatui is an optional evaluation candidate only if the narrow composer increases rendering risk; it is not selected by default.
|
|
20
20
|
|
|
@@ -76,4 +76,6 @@ Unverified, mismatched, or unsigned/unattested artifacts are not eligible for co
|
|
|
76
76
|
|
|
77
77
|
## Release artifact
|
|
78
78
|
|
|
79
|
-
`npm run build:terminal-host` (`scripts/development/build-terminal-host.mjs`)
|
|
79
|
+
`npm run build:terminal-host` (`scripts/development/build-terminal-host.mjs`) builds the library once with `cargo build --release --locked --lib --features node-addon`, copies the cdylib as `terminal-host.node`, loads the addon in Node, and checks its `version()` report: protocol, generation, crate version, pinned `libghostty-vt` commit, and the absence of `fault-injection` and `resident-fixtures`. Release and native validation runners invoke it with their pinned Zig toolchain. The normal developer `npm run build` invokes the same release-profile builder for the current platform; on Windows it links a manifest-verified terminal library exported by trusted CI and records the repository-pinned Zig producer version without invoking Zig on the workstation. The macOS x64 cross build may use the recorded Mach-O architecture check when the arm64 runner cannot load the addon.
|
|
80
|
+
|
|
81
|
+
The generated `terminal-host.manifest.json` uses schema `a1-terminal-host-artifact-v2` and records `kind: "node-addon"`, Node-API 9, the addon's SHA-256 and size, the release-relative role-entry digest, the notices digest, target triple, Rust and Zig versions, and `Cargo.lock` digest. The role entry passes only the fixed role grammar and records `{ executable: process.execPath, entry, artifact: addonPath }` once in the addon; the explicit loaded artifact identity makes release-hold reporting independent of development environment propagation. Detached and WMI resident starts use the recorded Node-and-entry command and never resolve an A1-built executable. The publication `package` job refuses to pack unless every platform's process-guardian and terminal-host addons match their manifests, and A1 verifies the addon, entry, and notices before loading the host.
|
|
@@ -26,19 +26,21 @@ Physical desktop automation is absent from the active repository baseline. Any f
|
|
|
26
26
|
The published package contains:
|
|
27
27
|
|
|
28
28
|
- the sole public `a1` command;
|
|
29
|
-
- the internal supervisor
|
|
30
|
-
- immutable release/bootstrap, lifecycle, protocol, storage, update, owned-UI, and Pi-adapter modules;
|
|
29
|
+
- the internal supervisor, launch-guardian, exit-watcher, and fixed terminal-host role entries;
|
|
30
|
+
- immutable release/bootstrap, lifecycle, protocol, storage, update, owned-UI, and Pi-adapter modules, plus `process-guardian.node` and `terminal-host.node` for every supported target with manifests and terminal-host notices;
|
|
31
31
|
- current user and architecture documentation.
|
|
32
32
|
|
|
33
|
-
It contains no
|
|
33
|
+
It contains no executable built from A1 sources, separate session-lock addon, browser/desktop GUI, physical automation driver, or generated runtime data. PTY ownership, terminal emulation, composition, and input translation exist only inside the terminal-host addon; the JavaScript product path does not relay terminal bytes.
|
|
34
34
|
|
|
35
|
-
## Native
|
|
35
|
+
## Native addon layer
|
|
36
36
|
|
|
37
37
|
`npm run doctor` reports this contract against the current machine—Node range, npm major, git, the GitHub CLI, Rust/Cargo, and whether `node_modules` matches the lockfile—and names the install command for anything missing. `npm run build` runs the same check as its first step, so an unmet prerequisite fails by name rather than inside a module resolver or Cargo. Unmet tool prerequisites block; a Node or npm version outside the declared contract is reported as an advisory without failing the build.
|
|
38
38
|
|
|
39
|
-
Source builds require Rust/Cargo 1.85 or newer; `rust-toolchain.toml` pins the exact version CI and publication install through `scripts/development/prepare-ci-rust.sh`, and rustup selects that version for Cargo commands inside the repository, installing it on first use. The Zig cache (`mlugg/setup-zig`, one entry per workflow and platform per run, bounded to 512 MiB) stays enabled: measured on 2026-10-16, a cold Zig build of `libghostty-vt` added about 40 to 60 seconds to a terminal-host test build (Linux 2m21s cold against 1m20s warm, Windows 2m05s against 1m26s), which the cache saves on every warm run.
|
|
39
|
+
Source builds require Rust/Cargo 1.85 or newer; `rust-toolchain.toml` pins the exact version CI and publication install through `scripts/development/prepare-ci-rust.sh`, and rustup selects that version for Cargo commands inside the repository, installing it on first use. The Zig cache (`mlugg/setup-zig`, one entry per workflow and platform per run, bounded to 512 MiB) stays enabled: measured on 2026-10-16, a cold Zig build of `libghostty-vt` added about 40 to 60 seconds to a terminal-host test build (Linux 2m21s cold against 1m20s warm, Windows 2m05s against 1m26s), which the cache saves on every warm run.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
`npm run build` compiles `native/process-guardian` as the host platform's Node-API 9 addon, places `process-guardian.node` under `dist/native/<platform>-<architecture>/`, loads it to observe its version identity, and writes the v2 integrity manifest. It also builds the current platform's release-profile `terminal-host.node`, whose v2 manifest binds the addon, `dist/foundation/resident-tabs/host-entry.js`, and notices, so the ordinary `npm run build && ./scripts/dev` handoff exercises resident tabs without deployment. On Windows the normal build reuses a compatible ignored terminal-library cache or downloads the newest compatible library from the current branch or a successful CI run belonging to a pull request already merged into `develop`, then links it under the crate's manifest verification; it never silently invokes the quarantined Zig package fetch. An unavailable or mismatched prerequisite fails the build instead of leaving a nominally successful single-agent-only tree. The npm `prepare` lifecycle and validation preflight use the explicit core build because selected native lanes assemble and certify terminal hosts separately.
|
|
42
|
+
|
|
43
|
+
Preview and stable candidate workflows build the addons on isolated Windows, Linux, and macOS arm64 runners, cross-compile the macOS x64 addons on the arm64 runner with `--target x86_64-apple-darwin`, and assemble all targets before packing. Those release runners retain their explicit `npm run build:terminal-host` step and pinned Zig input rather than consuming a workstation cache. Cross-compiled artifacts record a Mach-O architecture check when they cannot be loaded on the producing runner. Supported native code is always loaded by Node; an absent, incompatible, or unsupported addon fails before runtime startup rather than falling back to PID-only cleanup. The process-guardian addon inherits terminal handles but transports no terminal bytes. Its separate Node-hosted exit watcher restores terminal state after guardian failure, subject to the documented node-wide-kill gap. See [`process-guardian-provenance.md`](process-guardian-provenance.md), [`terminal-host-provenance.md`](terminal-host-provenance.md), and [`resident-tabs.md`](resident-tabs.md).
|
|
42
44
|
|
|
43
45
|
## Gates
|
|
44
46
|
|
|
@@ -193,7 +193,7 @@ validation; malformed, mixed, renamed, stale, or unavailable inputs fail closed.
|
|
|
193
193
|
A legitimate generated baseline update remains outside the allowlist and follows the
|
|
194
194
|
manually accepted mixed/code path.
|
|
195
195
|
|
|
196
|
-
A new implementation-bound specification starts as OpenSpec-only artifacts in one normally named draft PR. Explicit approval to implement continues in that same worktree, branch, history, and PR; the plan does not merge first. Approved refinements reconcile planning before code. After implementation, add one to ten plain implementation-specific bullets under `## Acceptance` and mark the PR ready; the trusted `OpenSpec finalization` workflow conservatively synchronizes deltas, stages the dated archive plus conditional acceptance manifest, and commits them to the same branch, and exact-head CI validates that head.
|
|
196
|
+
A new implementation-bound specification starts as OpenSpec-only artifacts in one normally named draft PR. Explicit approval to implement continues in that same worktree, branch, history, and PR; the plan does not merge first. Approved refinements reconcile planning before code. After implementation, add one to ten plain implementation-specific bullets under `## Acceptance` and mark the PR ready; the trusted `OpenSpec finalization` workflow conservatively synchronizes deltas, stages the dated archive plus conditional acceptance manifest, and commits them to the same branch, and exact-head CI validates that head. Automation-owned auto-merge remains disabled: an authorized maintainer's direct merge, personal exact-head auto-merge arm, or explicit request for the active agent to expected-head squash-merge the currently eligible candidate accepts the listed scenarios and atomically integrates implementation, specs, and archive. No acceptance or archive follow-up PR is created. Closing an unmerged draft integrates nothing; cleanup still needs separate approval. See [delivery and archive handoff](openspec-archive-automation.md) for commands and legacy compatibility.
|
|
197
197
|
|
|
198
198
|
## Numbered development previews
|
|
199
199
|
|