feat(process): persistent-process tier SDK surface#53
Conversation
Bump contracts to the merged astrid:process@1.0.0 persistent tier (#12 commit 1a06cf4 — process package only, leaves the interfaces bundle / hook-event-request untouched) and add the ergonomic wrapper in astrid-sdk/src/process.rs. Command gains spawn_persistent() + the persistent knobs (label/keep_stdin_open/overflow/log_ring_bytes/max_lifetime/idle_timeout/exit_retention/limits). New PersistentProcess handle (status/read_logs/read_since/write_stdin/close_stdin/signal/wait/stop/release), process::{attach,list,status_many}, ProcessId/ProcessInfo/ProcessPhase/LogStream/LogCursor/LogChunk/OverflowPolicy/ResourceLimits types, Signal::{Stop,Cont}. attach is an id-wrapper (reattach works without the host's deferred attach fn).
There was a problem hiding this comment.
Code Review
This pull request introduces the persistent-process tier to the process module, mirroring the host astrid:process@1.0.0 specification. It adds Command::spawn_persistent, the PersistentProcess handle, and associated management functions such as attach, list, and status_many, along with supporting types like ProcessId, LogCursor, and ProcessInfo. Feedback on the changes highlights a compilation error in status_many due to incorrect WIT-binding types, and recommends deriving serde::Serialize and serde::Deserialize on ProcessId and LogCursor to facilitate state persistence.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
Pull request overview
Adds the capsule-facing Rust SDK surface for the astrid:process@1.0.0 persistent-process tier, enabling background child processes that can outlive the pooled/stateless instance that spawned them and be reattached via an opaque ProcessId.
Changes:
- Updates the staged
astrid:process@1.0.0WIT contract to include the persistent tier types/functions and expanded spawn request fields. - Extends
astrid_sdk::process::Commandwith persistent-only builder knobs and a newspawn_persistent()terminal. - Introduces
PersistentProcessplus supporting types (ProcessId,ProcessInfo,ProcessPhase, log cursor/chunk types, overflow/limits types) and module functions (attach,list,status_many).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| CHANGELOG.md | Documents the new persistent-process API surface and related types. |
| astrid-sys/wit-staging/deps/astrid-process/process@1.0.0.wit | Bumps/restages the process WIT contract to include persistent tier capabilities and new request fields. |
| astrid-sdk/src/process.rs | Implements the ergonomic persistent-process API and wires new spawn request fields into Command::into_wit. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…#55) ## What Adds `capabilities::enumerate()` to `astrid-sdk` — the ergonomic wrapper for `astrid:sys/host.enumerate-capabilities` (astrid-runtime/wit#13, merged), implemented kernel-side in unicity-astrid/astrid (`feat(sys): implement enumerate-capabilities + complete check-capsule-capability`). It is the infallible list dual of `capabilities::check`: returns the calling capsule's own held capability names — the categories declared in its `[capabilities]` manifest block (`host_process`, `net_connect`, `fs_read`, …), not the scoped arguments within them. Argument-free, returns `Vec<String>` with no error path (an empty list is the valid "no capabilities" answer), so a reusable capsule can ground its behaviour in what it can actually do instead of hard-coding it. The `contracts` submodule is bumped to the merged `9742f80` so `astrid-sys` regenerates the binding; `astrid-sys/wit-staging` restaged. ## Stacking Stacked on `feat/process-persistent-sdk` (#53) — base is that branch, so the diff shows only the enumerate delta. Rebase onto `main` and retarget once #53 lands. ## Verify `cargo build --workspace` and `cargo clippy --workspace --all-targets` green.
… persist-unsupported Derives Serialize/Deserialize on ProcessId and LogCursor so a capsule can persist the reattach id and a resumable log cursor (e.g. as one KV state struct) and resume read_since from the same position in a later invocation — LogCursor's token was private with no accessor, so it could not be persisted at all before. ProcessId stays an opaque handle, not a credential: a leaked id is inert across the principal/capsule boundary (the host re-checks ownership on every id-keyed call), so exposing it for serialization is safe. Also documents that spawn_persistent can fail with persist-unsupported even when host_process is granted — when the invocation has no authenticated principal in scope (owner-fallback), since a persistent id must be scoped to a real principal.
The astrid-contracts.wit events mirror was stale relative to the contracts submodule (at 9742f80, unchanged): it was missing astrid:hook's hook-event-request record, which landed in contracts/interfaces when the pin moved to include it. Regenerated via scripts/sync-contracts-wit.sh so sync-contracts-wit.sh --check passes. Only the mirror is touched — the submodule pin is untouched.
## Summary Mirrors the Rust SDK and the host `astrid:process@1.0.0` **persistent-process tier** (host: astrid-runtime/astrid#867; Rust SDK: astrid-runtime/sdk-rust#53; contract: astrid-runtime/wit#12), so JS/TS capsule authors can spawn a background child that **outlives the pooled, stateless instance** that started it — unlike `process.BackgroundProcessHandle`, whose kernel resource is reaped on instance reset. ## Changes - **`contracts` submodule → the merged `astrid:process@1.0.0` persistent tier** (wit commit `1a06cf4`, the #12 squash). Deliberately the **#12-only** commit (predates #11), so the `astrid:contracts` events bundle (`astrid-contracts.wit` / generated `contracts.ts`) is untouched (`sync-contracts-wit.sh --check` stays green) and unrelated open SDK PRs are unaffected. On `main`, `astrid-build` reads the host WIT **live from `contracts/host/`** (`CANONICAL_WIT_DIR`), so the bump is all ComponentizeJS needs. - **`wit-imports.d.ts`** (hand-written host ABI types): persistent types + free-function declarations on `astrid:process/host@1.0.0` — `spawnPersistent`, `attach`, `listProcesses`, `status`, `statusMany`, `readLogs`, `readSince`, `writeStdin`, `closeStdin`, `signal`, `wait`, `stop`, `releaseProcess`, `watch`, `unwatch`; `ProcessInfo`, `ProcessPhase`, `LogStream`, `LogCursor`, `LogChunk`, `OverflowPolicy`, `ResourceLimits`; `ErrorCode` gains `no-such-process` / `registry-full` / `persist-unsupported`; `ProcessSignal` gains `stop` / `cont`; `SpawnRequest` gains the 8 persistent fields. - **`process.ts`** ergonomic layer: `spawnPersistent(cmd, args, options)` + the persistent `SpawnPersistentOptions` knobs; `PersistentProcess` class (`status` / `readLogs` (drain) / `readSince` (non-draining cursor → byte-faithful `LogChunkResult`; `logCursorStart()`) / `writeStdin` / `closeStdin` / `signal` / `wait` (bounded) / `stop` (consumes — SIGTERM→grace→SIGKILL) / `release`); module fns `attach(id)` (id-wrapper reattach — works without the host's deferred `attach` resource fn), `listProcesses`, `statusMany`; ergonomic types. `buildSpawnRequest` updated for the new `SpawnRequest` fields (required). - **`index.ts`**: top-level re-exports for the new public types. ## Not exposed (host-deferred) `watch` / `unwatch` (open publish-authority RFC question — poll via `status` + bounded `wait`) and resource-limit enforcement (`limits` is plumbed through but the host doesn't enforce it yet). ## Test Plan - [x] `npm run build` (tsc) green across the workspace - [x] **End-to-end:** the `examples/test-capsule` **componentizes** against the persistent WIT — ComponentizeJS produced a 12.96 MB WASM with **169 host imports**, proving the new host functions wire through the JS build path - [x] `scripts/sync-contracts-wit.sh --check` passes (events bundle untouched) - [x] Git diff scoped to 5 files (contracts pointer + 3 SDK sources + CHANGELOG); no `contracts.ts` drift
Rebuilt on main post-#53/#56 squash-merge: the branch previously carried its own pre-squash copy of the feature diff; now it is the bump + CHANGELOG only. - Workspace 0.7.0 -> 0.7.1 (+ the three internal dependency pins). - CHANGELOG: [Unreleased] -> [0.7.1] - 2026-06-10, with a release synopsis (extracted into the GitHub release body by release.yml), the previously missing Fixed entry for the #56 tool_describe fan-out fix, and PR attribution on the #53 entries.
## Summary `chore: release` — bumps the workspace to **0.7.1**, the patch release that restores capsule tool discovery (#56) and ships the post-0.7 additive surfaces (#53). Per the repo rule, a version bump is its own PR. The branch was originally stacked on #53; after #53 and #56 squash-merged it has been **rebuilt on `main`** so the diff is exactly the bump + CHANGELOG. No breaking changes — `astrid-sdk = "0.7"` consumers resolve 0.7.1 automatically once published, so capsules pick up the tool_describe fix with a rebuild and **no** dep bump. ## Changes - `[workspace.package].version` `0.7.0` → `0.7.1` (+ the three internal `[workspace.dependencies]` pins: `astrid-sdk`, `astrid-sdk-macros`, `astrid-sys`). - CHANGELOG: `[Unreleased]` → `[0.7.1] - 2026-06-10` with: - a release **synopsis** paragraph (release.yml extracts the whole `## [0.7.1]` block into the GitHub release body on tag), - the previously **missing `Fixed` entry for #56** (the tool_describe describe-fan-out fix — the fleet-critical one), - PR attribution on the #53 `Added` entries (`capabilities::enumerate`, persistent-process tier). ## Release steps (maintainer) 1. Merge this PR. 2. `cargo publish` in dependency order: `astrid-sys`, `astrid-sdk-macros`, `astrid-sdk`. 3. Tag `v0.7.1` on the merge commit → release.yml creates the GitHub release from the CHANGELOG block.
Summary
Adds the ergonomic SDK surface for the persistent-process tier of
astrid:process@1.0.0(host side: astrid-runtime/astrid#867; contract: astrid-runtime/wit#12), so capsule authors can spawn a background child that outlives the pooled, stateless instance that started it — unlikeprocess::Process, whose kernel resource is reaped on instance reset.Changes
contractssubmodule → the mergedastrid:process@1.0.0persistent tier (wit commit1a06cf4, the feat(net): mpsc-style channel API — recv/send/try_recv/try_accept #12 squash). Scoped deliberately to the process package:1a06cf4predates feat(net): mpsc-style channel API — recv/send/try_recv/try_accept #11, so theinterfaces/bundle (astrid-sdk/wit/astrid-contracts.wit) is untouched and the open hook-event-request PR feat(contracts): regenerate bundle with hook-event-request #50 is unaffected (sync-contracts-wit.sh --checkstays green).astrid-sys/build.rsrestageswit-staging/deps/astrid-process/; the rawwit_process::*bindings regenerate.astrid-sdk/src/process.rsergonomic layer:Commandgains the persistent-only builder knobs (label,keep_stdin_open,overflow,log_ring_bytes,max_lifetime,idle_timeout,exit_retention,limits) and aspawn_persistent()terminal →PersistentProcess.PersistentProcess(keyed by an opaqueProcessId):status,read_logs(drain),read_since(non-draining cursor, byte-faithfulLogChunk),write_stdin,close_stdin,signal,wait(bounded),stop(consumes),release(consumes).attach(id)— an id-wrapper so reattach from a later invocation works without the host's deferredattachresource fn (the first id-keyed call validates ownership) — pluslistandstatus_many.ProcessId,ProcessInfo,ProcessPhase,LogStream,LogCursor,LogChunk,OverflowPolicy,ResourceLimits.SignalgainsStop/Cont.Command::into_witupdated for the newSpawnRequestfields (a required fix — the WIT bump added 8 fields).Not exposed (host-deferred)
watch/unwatch(open publish-authority RFC question — poll viastatus+ boundedwait) and resource-limit enforcement (limitsis plumbed through but the host does not enforce it yet).Test Plan
cargo build --workspace+cargo test --workspacegreenastrid-sdk(clean); the 2astrid-sdk-macrosbacktick warnings are pre-existing and untouchedcargo fmt --all --checkcleanscripts/sync-contracts-wit.sh --checkpasses (interfaces bundle untouched — feat(contracts): regenerate bundle with hook-event-request #50 unaffected)