merge: worker runtime orchestration

This commit is contained in:
Keisuke Hirata 2026-06-29 11:53:52 +09:00
commit 11d94d3866
No known key found for this signature in database
79 changed files with 3016 additions and 1093 deletions

View File

@ -0,0 +1,2 @@
{"id":"orch-plan-20260628-171232-1","ticket_id":"00001KW76E8EG","kind":"blocked_by","related_ticket":"00001KW7726H9","note":"`00001KW76E8EG` depends on `00001KW7726H9`; embedded Runtime fs-store persistence should persist the canonical Runtime Worker creation model rather than an embedded-only bypass. `00001KW7726H9` is currently inprogress, so this Ticket remains queued and must not start a child worktree/Pod until that dependency is merged/validated/closed or an explicit replan changes the dependency.","author":"yoi-orchestrator","at":"2026-06-28T17:12:32Z"}
{"id":"orch-plan-20260628-184007-2","ticket_id":"00001KW76E8EG","kind":"accepted_plan","note":"`00001KW7726H9` is now closed after merge/validation, so this Ticket's dependency is resolved. `00001KW7835H0` remains queued and is related to this fs-store migration; run fs-store persistence first before old Pod crate removal.","accepted_plan":{"summary":"`00001KW7726H9` が closed になったため、Workspace Backend embedded Runtime を memory-backed から `worker-runtime` fs-store backed に切り替える。Runtime store root は user data 配下の workspace-id 別 local state とし、Browser-facing API には出さない。Backend restart 後に catalog / worker snapshot / transcript / ConfigBundle store が復元され、live execution handle は connected として偽装せず stale/unconnected diagnostic として扱う。","branch":"work/00001KW76E8EG-embedded-runtime-fs-store","worktree":"/home/hare/Projects/yoi/.worktree/00001KW76E8EG-embedded-runtime-fs-store","role_plan":"Orchestrator が dedicated child worktree を作成し、coder Worker に `crates/workspace-server`, `crates/worker-runtime`, 必要に応じて `crates/worker`, root Cargo/package files の focused write scope を委譲する。reviewer Worker は read-only で fs-store feature enablement、ServerConfig store-root authority、restart restore behavior、ConfigBundle/transcript restore、stale execution diagnostic、Browser-facing non-leak、memory-backed production bypass absence を確認する。merge/validation/done/cleanup は Orchestrator が行う。現時点では `StopPod` は使用せず、cleanup は child worktree/branch のみ行う。"},"author":"yoi-orchestrator","at":"2026-06-28T18:40:07Z"}

View File

@ -1,8 +1,8 @@
---
title: 'Workspace Backendのembedded Runtimeをfs-store永続化に切り替える'
state: 'queued'
state: 'closed'
created_at: '2026-06-28T13:34:29Z'
updated_at: '2026-06-28T17:12:16Z'
updated_at: '2026-06-28T19:16:51Z'
assignee: null
queued_by: 'workspace-panel'
queued_at: '2026-06-28T17:12:16Z'

View File

@ -0,0 +1,27 @@
Workspace Backend embedded Runtime を memory-backed から fs-store backed `worker-runtime` へ切り替え、reviewer approval 後に orchestration branch へ merge した。
実装内容:
- `workspace-server``worker-runtime``fs-store` feature を有効化。
- `ServerConfig``embedded_runtime_store_root` を追加。
- default store root を user data 配下の `workspace-server/<workspace_id>/embedded-runtime` に設定し、project records / `.yoi/tickets` と混ぜない設計にした。
- 通常の `WorkspaceApi::new` / `new_with_execution_backend` 経路を fs-store backed `EmbeddedWorkerRuntime::new_fs_store_with_execution_backend(...)` に切り替え。
- `Runtime::with_fs_store_and_execution_backend(...)` を使う constructor を追加。
- Backend restart 後に Runtime catalog / Worker snapshot / transcript / ConfigBundle store が復元されることを focused tests で確認。
- restart 後の live execution handle は connected として偽装せず、stale/unconnected diagnostic/projection として扱う。
- Browser-facing API に Runtime store root / raw path / execution handle を出さないことを確認。
- production path が memory-backed runtime に戻らないよう通常 constructor を fs-store backed にした。
Integrated commit:
- `736b05c6 feat: persist embedded workspace runtime in fs store`
- merge: `888e7b68 merge: embedded runtime fs-store`
Validation:
- `cargo fmt --all --check`: success
- `cargo test -p yoi-workspace-server`: success (`38 passed`)
- `cargo test -p worker-runtime --features "ws-server fs-store"`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Operational note:
- User instructed not to use `StopPod` for now after repeated stop/hang symptoms. Cleanup will skip role-Pod shutdown and remove only child implementation worktree / branch.

View File

@ -54,4 +54,318 @@ Marked ready by `yoi ticket state`.
Ticket を `workspace-panel` が queued にしました。
---
<!-- event: decision author: yoi-orchestrator at: 2026-06-28T17:12:39Z -->
## Decision
Routing decision:
Workspace Dashboard Queue authorized Orchestrator routing, so the Ticket and current workspace state were inspected.
Findings:
- Ticket state: `queued`
- Outgoing dependency: `depends_on 00001KW7726H9`
- Blocking Ticket `00001KW7726H9` is currently `inprogress`.
- Current inprogress Tickets: `00001KW7726H9`
- Orchestration worktree: clean
Decision:
- Leave `00001KW76E8EG` queued and do not create a child worktree / role Pod yet.
- Reason: embedded Runtime fs-store persistence should persist the canonical Runtime Worker creation model, not an embedded-only bypass. That canonical creation path is currently being implemented in `00001KW7726H9`.
Next routing condition:
- Re-evaluate this Ticket after `00001KW7726H9` is merged, validated, and closed, or after an explicit replan changes the dependency.
---
<!-- event: decision author: yoi-orchestrator at: 2026-06-28T18:40:18Z -->
## Decision
Routing decision:
`00001KW7726H9` has been merged, validated, closed, and cleaned up, so this Ticket's dependency is now resolved. Current workspace state was inspected before implementation side effects.
Findings:
- Ticket state: `queued`
- Dependencies on `00001KW55B33B` and `00001KW7726H9` are resolved/closed.
- Current `inprogress` Tickets: 0
- Orchestration worktree: clean
- `00001KW7835H0` remains queued and is related to this fs-store migration. Run embedded Runtime fs-store persistence first, then re-evaluate old Pod crate removal.
Decision:
- Accept this Ticket for implementation and transition `queued -> inprogress` before child worktree / Pod side effects.
Plan:
- Worktree: `/home/hare/Projects/yoi/.worktree/00001KW76E8EG-embedded-runtime-fs-store`
- Branch: `work/00001KW76E8EG-embedded-runtime-fs-store`
- Coder Worker will implement the Workspace Backend embedded Runtime fs-store switch and focused restart/persistence tests.
- Reviewer Worker will verify fs-store feature enablement, ServerConfig store-root authority, restart restore behavior, ConfigBundle/transcript restore, stale execution diagnostic, Browser-facing non-leak, and production memory-backed bypass absence.
Operational note:
- User instructed not to use `StopPod` for now after repeated stop/hang symptoms. Cleanup will skip role-Pod shutdown and remove only child worktree/branch unless explicitly reauthorized.
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-28T18:40:23Z from: queued to: inprogress reason: dependencies_closed_and_queue_continues field: state -->
## State changed
Dependencies `00001KW55B33B` and `00001KW7726H9` are closed, no Ticket is currently inprogress, and the orchestration worktree is clean. Accepting implementation before child worktree / role Pod side effects.
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T18:40:58Z -->
## Implementation report
Implementation kickoff:
- Worktree created: `/home/hare/Projects/yoi/.worktree/00001KW76E8EG-embedded-runtime-fs-store`
- Branch: `work/00001KW76E8EG-embedded-runtime-fs-store`
- Coder Worker spawned: `yoi-coder-00001KW76E8EG-fs-store`
Scope notes:
- Coder write scope is limited to `crates/workspace-server/**`, `crates/worker-runtime/**`, `crates/worker/**`, root `Cargo.toml`, `Cargo.lock`, and `package.nix` if needed.
- Coder is instructed not to modify Ticket records and not to operate in root/orchestration worktrees.
Implementation focus:
- enable `worker-runtime` `fs-store` feature in `workspace-server`
- add `ServerConfig` embedded Runtime store root
- default store root under user data / workspace id, not under project records or `.yoi/tickets`
- add fs-store backed `EmbeddedWorkerRuntime` constructor using `Runtime::with_fs_store_and_execution_backend`
- normal WorkspaceApi constructor uses fs-store, not `new_memory_with_execution_backend`
- restart restores Runtime catalog / Worker snapshot / transcript / ConfigBundle store
- live execution handle is not falsely connected after restart; stale/unconnected diagnostic/projection remains explicit
- Browser-facing APIs do not expose store root / raw path / execution handle
- tests cover fs-store restore, ConfigBundle/transcript restore, execution stale/unconnected restore, memory-backed production bypass absence, and store-root non-leak
---
<!-- event: implementation_report author: yoi-coder-00001KW76E8EG-fs-store at: 2026-06-28T19:05:47Z -->
## Implementation report
Implementation report:
実装・検証・commit 完了。Ticket ファイル編集、merge、close、cleanup は未実施。
Commit:
- `736b05c6 feat: persist embedded workspace runtime in fs store`
変更概要:
- `workspace-server``worker-runtime``fs-store` feature を有効化。
- `ServerConfig``embedded_runtime_store_root` を追加し、既定値を user data 配下の `workspace-server/<workspace_id>/embedded-runtime` に設定。
- 通常の `WorkspaceApi` 生成経路を `EmbeddedWorkerRuntime::new_memory_with_execution_backend` から `new_fs_store_with_execution_backend` に切り替え。
- `Runtime::with_fs_store_and_execution_backend` を使う fs-store backed constructor を追加。
- 再起動後の Worker は live handle 接続済みと偽らず、`stale` / `unconnected` の診断を Browser API projection に残すようにした。
- store root / raw path / handle が Browser API に出ないことをテスト。
- `package.nix``cargoHash` を更新。
追加・更新テスト:
- fs-store restart restore:
- catalog / Worker snapshot
- ConfigBundle
- transcript
- stale execution projection / input rejection
- store root isolation / non-leak
- 既存 workspace-server tests は明示的 temp store root を使うよう調整。
Validation:
- `cargo fmt`: success
- `cargo fmt -- --check`: success
- `cargo test -p yoi-workspace-server`: success
- `cargo test -p worker-runtime --features "ws-server fs-store"`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi`: success
作業後状態:
- branch: `work/00001KW76E8EG-embedded-runtime-fs-store`
- `git status --short`: clean
Residual risks:
- 特記すべき未解決リスクなし。再起動後の実行 handle は復元せず stale/unconnected として扱う設計どおり。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T19:06:09Z -->
## Implementation report
Review kickoff:
Reviewer Worker spawned for `00001KW76E8EG` implementation.
- Reviewer Worker: `yoi-reviewer-00001KW76E8EG-fs-store`
- Review target commit: `736b05c6 feat: persist embedded workspace runtime in fs store`
- Review mode: read-only implementation worktree review
Review focus:
- `workspace-server` enables `worker-runtime` `fs-store` feature
- `ServerConfig` has authoritative embedded Runtime store root defaulting under user data / workspace id, not project records
- normal `WorkspaceApi` uses fs-store backed `EmbeddedWorkerRuntime`, not memory-backed runtime
- restart restores catalog / Worker snapshot / ConfigBundle / transcript
- execution handle after restart is stale/unconnected diagnostic, not falsely connected
- Browser APIs do not leak store root / raw path / execution handle
- production path cannot accidentally use memory-backed runtime
- focused tests cover these behaviors
---
<!-- event: review author: yoi-reviewer-00001KW76E8EG-fs-store at: 2026-06-28T19:15:12Z status: approve -->
## Review: approve
approve
Ticket `00001KW76E8EG` の意図・受け入れ条件に対して、実装は妥当です。ブロッカーは見つかりませんでした。
## 確認した主な根拠
- `workspace-server``worker-runtime``fs-store` feature を有効化していることを確認。
- `crates/workspace-server/Cargo.toml`
- `ServerConfig``embedded_runtime_store_root` が追加され、通常の `local_dev` 設定では project records / `.yoi/tickets` ではなく、ユーザーデータ配下の workspace id ベースの領域を既定 root にしていることを確認。
- `crates/workspace-server/src/server.rs`
- 通常の `WorkspaceApi::new` / `new_with_execution_backend` 経路が `EmbeddedWorkerRuntime::new_fs_store_with_execution_backend(...)` を使い、memory store ではなく fs-store backed runtime を構築していることを確認。
- `crates/workspace-server/src/server.rs`
- `crates/workspace-server/src/hosts.rs`
- `EmbeddedWorkerRuntime` 側で `EmbeddedRuntimeStore::fs_store(store_root)` + `WorkerRuntime::with_options(..., RuntimeOptions::ws_server())` を使う経路が追加されていることを確認。
- restart 後の復元について、テストで以下が確認されていることを確認。
- catalog / worker snapshot が復元される
- `ConfigBundle` が復元される
- transcript が復元される
- restart 後の execution handle は接続済み扱いにならず、stale/unconnected diagnostic となる
- restart 後に input を送っても接続済み実行 handle として扱われない
- Browser/API projection について、serialized response に store root/raw path/handle などが混入しないことを確認するテストが追加されていることを確認。
- production 経路については、通常 `WorkspaceApi` の生成経路が fs-store を使っており、memory-backed runtime に戻る変更は見当たりませんでした。memory constructor は低レベル/テスト用途として残っていますが、通常 server path からは使われていません。
## 実行した検証
read-only 方針に合わせ、worktree 内のソース/Ticket は変更せず、`CARGO_TARGET_DIR` を `/run/user/.../target-review` に逃がして検証しました。
- `git diff --check HEAD^ HEAD`: success
- `cargo fmt --all -- --check`: success
- `CARGO_TARGET_DIR=/run/user/1000/yoi/yoi-reviewer-00001KW76E8EG-fs-store/target-review cargo test -p yoi-workspace-server`: success
- `CARGO_TARGET_DIR=/run/user/1000/yoi/yoi-reviewer-00001KW76E8EG-fs-store/target-review cargo test -p worker-runtime --features ws-server,fs-store`: success
- `CARGO_TARGET_DIR=/run/user/1000/yoi/yoi-reviewer-00001KW76E8EG-fs-store/target-review cargo check -p yoi`: success
検証後の `git status --short` も clean でした。
## 残リスク
- `nix build .#yoi` は reviewer 側では再実行していません。ただし `Cargo.lock` / `package.nix` の更新と Rust 側の check/test は整合しており、報告されているビルド系コマンドは十分 plausible と判断します。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T19:16:09Z -->
## Implementation report
Merge / validation report:
Implementation branch was integrated into the orchestration branch after reviewer approval.
Integrated commit:
- `736b05c6 feat: persist embedded workspace runtime in fs store`
Merge commit:
- `888e7b68 merge: embedded runtime fs-store`
Validation in orchestration worktree:
- `cargo fmt --all --check`: success
- `cargo test -p yoi-workspace-server`: success (`38 passed`)
- `cargo test -p worker-runtime --features "ws-server fs-store"`: success (`26 passed` + rest server tests)
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Outcome:
- Workspace Backend embedded Runtime now uses fs-store backed `worker-runtime` for normal `WorkspaceApi` construction.
- `ServerConfig` carries `embedded_runtime_store_root`, with default user-data/workspace-id local state path rather than project records / `.yoi/tickets`.
- Restart restores catalog / Worker snapshot / ConfigBundle / transcript.
- Restart does not falsely restore live execution handles as connected; stale/unconnected diagnostic/projection remains explicit.
- Browser-facing APIs do not expose Runtime store root / raw path / execution handle.
- Production path no longer uses `new_memory_with_execution_backend`; memory constructor remains for explicit low-level/test use only.
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-28T19:16:18Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
## State changed
Reviewer approval後に implementation branch を orchestration branch へ merge し、要求された validation が成功したため done に遷移する。
Validation:
- `cargo fmt --all --check`: success
- `cargo test -p yoi-workspace-server`: success (`38 passed`)
- `cargo test -p worker-runtime --features "ws-server fs-store"`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
---
<!-- event: state_changed author: hare at: 2026-06-28T19:16:31Z from: done to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-28T19:16:31Z status: closed -->
## 完了
Workspace Backend embedded Runtime を memory-backed から fs-store backed `worker-runtime` へ切り替え、reviewer approval 後に orchestration branch へ merge した。
実装内容:
- `workspace-server``worker-runtime``fs-store` feature を有効化。
- `ServerConfig``embedded_runtime_store_root` を追加。
- default store root を user data 配下の `workspace-server/<workspace_id>/embedded-runtime` に設定し、project records / `.yoi/tickets` と混ぜない設計にした。
- 通常の `WorkspaceApi::new` / `new_with_execution_backend` 経路を fs-store backed `EmbeddedWorkerRuntime::new_fs_store_with_execution_backend(...)` に切り替え。
- `Runtime::with_fs_store_and_execution_backend(...)` を使う constructor を追加。
- Backend restart 後に Runtime catalog / Worker snapshot / transcript / ConfigBundle store が復元されることを focused tests で確認。
- restart 後の live execution handle は connected として偽装せず、stale/unconnected diagnostic/projection として扱う。
- Browser-facing API に Runtime store root / raw path / execution handle を出さないことを確認。
- production path が memory-backed runtime に戻らないよう通常 constructor を fs-store backed にした。
Integrated commit:
- `736b05c6 feat: persist embedded workspace runtime in fs store`
- merge: `888e7b68 merge: embedded runtime fs-store`
Validation:
- `cargo fmt --all --check`: success
- `cargo test -p yoi-workspace-server`: success (`38 passed`)
- `cargo test -p worker-runtime --features "ws-server fs-store"`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Operational note:
- User instructed not to use `StopPod` for now after repeated stop/hang symptoms. Cleanup will skip role-Pod shutdown and remove only child implementation worktree / branch.
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T19:16:51Z -->
## Implementation report
Cleanup report:
- User instructed not to use `StopPod` for now after repeated stop/hang symptoms. Role Pod shutdown was intentionally skipped.
- Child implementation worktree was removed:
- `/home/hare/Projects/yoi/.worktree/00001KW76E8EG-embedded-runtime-fs-store`
- Child implementation branch was removed:
- `work/00001KW76E8EG-embedded-runtime-fs-store`
- No root/original workspace cleanup was performed.
- Orchestration worktree is clean after worktree/branch cleanup.
Routing note:
- `00001KW7835H0` can now be re-evaluated because its related/dependency prerequisites in this queue chain are complete.
---

View File

@ -1,8 +1,8 @@
---
title: 'Runtime Worker起動経路を正規のExecution/ConfigBundle経路に一本化する'
state: 'inprogress'
state: 'closed'
created_at: '2026-06-28T13:35:21Z'
updated_at: '2026-06-28T16:49:08Z'
updated_at: '2026-06-28T18:39:24Z'
assignee: null
queued_by: 'workspace-panel'
queued_at: '2026-06-28T16:47:42Z'

View File

@ -0,0 +1,33 @@
Runtime Worker 起動経路を canonical `ConfigBundleRef + ExecutionBackend` 経路へ一本化し、reviewer approval 後に orchestration branch へ merge した。
実装内容:
- Runtime `CreateWorkerRequest``config_bundle_ref` と user-only initial input 中心の内部作成契約へ整理。
- Browser-facing launch semantics と Runtime worker creation request を分離。
- raw workspace / cwd / tool scope / config store / secret / socket / path 類を Runtime create request に含めない境界にした。
- ConfigBundle missing / digest mismatch / profile mismatch を typed error / diagnostic として扱う。
- execution backend 未接続では Worker 作成を拒否し、input-capable Worker が backend 未接続になる経路を塞いだ。
- create 成功時のみ catalog / transcript / event を永続化し、spawn / initial input dispatch rejection は rollback。
- execution binding identity を raw handle / secret / path / socket を含まない non-authority projection として永続化。
- restore 後に live handle がない persisted execution mapping は `stale` として diagnostic `worker_execution_mapping_stale` を出す。
- System initial input を Runtime boundary で拒否し、launch/create が system transcript を注入できないようにした。
- Workspace embedded / Companion / remote-facing creation を canonical Runtime create path に寄せた。
- Remote Runtime projection でも execution status を見て `can_accept_input` / `can_stop` を計算し、stale / unconnected / rejected / errored Workers を input-capable として出さないようにした。
- fake/providerless response bypass は導入していない。
Integrated commits:
- `14bb4934a6374eea64591035e5342088ab0ccd09 runtime: unify worker creation path`
- `c29d10b67bfff1f4a7a1b2742ec05fe63b80c054 runtime: persist execution binding projection`
- `ba7f9d2ee83a946820cc234e847b6531b4a141f3 workspace: respect remote execution projection`
- merge: `bdb339fa merge: runtime worker launch unification`
Validation:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo test -p worker-runtime --features "fs-store ws-server"`: success
- `cargo test -p yoi-workspace-server`: success (`36 passed`)
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Operational note:
- User instructed not to use `StopPod` for now after repeated stop/hang symptoms. Cleanup will skip role-Pod shutdown and remove only child implementation worktree / branch.

View File

@ -277,3 +277,444 @@ Implementation focus:
- final request fields must be justified against source / scope-access / visibility / persistence-projection / retry / validation / existing type / failure.
---
<!-- event: implementation_report author: yoi-coder-00001KW7726H9-launch-unify at: 2026-06-28T17:46:07Z -->
## Implementation report
Implementation report:
実装・検証・commit 完了。merge / close / cleanup は未実施。
Commit:
- `14bb4934a6374eea64591035e5342088ab0ccd09 runtime: unify worker creation path`
実装概要:
- Runtime Worker 作成 API を canonical な `ConfigBundle + ExecutionBackend` 経路に統一。
- `worker-runtime::catalog::CreateWorkerRequest` を Runtime 内部の作成契約として絞り込み。
- 採用フィールド:
- `profile: ProfileSelector`
- `config_bundle: ConfigBundleRef`(必須)
- `initial_input: Option<WorkerInput>`
- 除外フィールド:
- Browser / product intent
- requested capabilities
- workspace refs / mount refs
- cwd / workspace path / tool scope / config store / secret / socket / raw path 類
- `CreateWorkerRequest::default()` / providerless / tools-less 作成経路を廃止し、input-capable Worker 作成には execution backend を必須化。
- ConfigBundle 未同期 / digest mismatch / profile 未宣言 / execution backend 不在 / spawn-input dispatch rejection を typed diagnostics / errors として扱うように変更。
- 作成は成功時のみ catalog / transcript / event を永続化し、spawn / initial input dispatch 失敗時は in-memory Worker と create event を rollback。
- Workspace 側で Browser-facing launch semantics と Runtime create request を分離。
- Browser-facing `WorkerSpawnRequest` は intent / requested name / acceptance / profile / initial input の範囲に限定。
- Workspace host が backend-resolved ConfigBundle を同期し、その `ConfigBundleRef` で Runtime create を呼び出す。
- Embedded / Companion / remote-facing creation が同じ Runtime create path を通るように調整。
- providerless / fake response 前提のテストを更新し、execution backend を持つ経路または typed rejection を確認するテストに変更。
API field rationale:
- `profile`
- source: launch backend が決定する profile selector
- visibility: opaque selector であり raw config/body/path/secret を含まない
- validation: synced ConfigBundle が selector を宣言していることを Runtime 側で検証
- persistence: Worker projection に durable selector として保存可能
- `config_bundle`
- source: Runtime create 前に backend が `store_config_bundle` / remote sync で確定する opaque ref
- visibility: `id + digest` のみで config store path や secret は露出しない
- validation: missing / digest mismatch / profile mismatch を typed error 化
- retry: 同じ bundle ref で再試行可能
- failure: missing ConfigBundle は create 前に fail closed
- `initial_input`
- source: Browser/product launch intent から通常の Worker input として明示的に渡す
- visibility: ユーザー/システム input 本体のみ。scope/path/secret は含めない
- persistence: 作成成功時に transcript に同一 transaction として保存
- failure: initial dispatch が execution backend に rejected された場合は create rollback
- 除外フィールド:
- `intent`, `requested_capabilities`, `workspace_refs`, `mount_refs`, raw cwd/path/config/secret は Runtime create API から除外
- 理由: Browser/product/backend policy 層の semantics であり、Runtime Worker の canonical durable create request に混ぜると visibility/persistence/retry/error 境界が崩れるため
Validation:
- `cargo fmt`: success
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo test -p yoi-workspace-server`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi`: success
Residual risks / review focus:
- E2E の実プロセス spawn テストは既存方針どおり未整備なので、実 worker process 起動の完全統合は unit/integration レベルの backend mock と `nix build` での確認に留まる。
- create 成功後の永続化失敗時に、すでに execution backend が外部 worker を spawn 済みであるケースの完全補償は今後さらに詰める余地がある。今回の実装では spawn / initial input rejection は rollback し、成功時だけ durable persistence する。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T17:46:31Z -->
## Implementation report
Review kickoff:
Reviewer Worker spawned for `00001KW7726H9` implementation.
- Reviewer Worker: `yoi-reviewer-00001KW7726H9-launch-unify`
- Review target commit: `14bb4934a6374eea64591035e5342088ab0ccd09 runtime: unify worker creation path`
- Review mode: read-only implementation worktree review
Review focus:
- Runtime worker creation uses canonical ConfigBundle + ExecutionBackend path
- Browser-facing launch semantics are separated from Runtime `CreateWorkerRequest`
- create request fields are justified against source / scope-access / visibility / persistence-projection / retry / validation / existing type / failure
- no raw workspace / cwd / tool scope / config store / secret / socket / path leaks
- ConfigBundle missing / digest mismatch / profile mismatch / execution backend missing / provider-secret failure produce typed diagnostics
- input-capable Workers require execution backend
- create is persistent/transactional enough, with rollback on spawn / initial input rejection
- embedded / Companion / remote-facing creation share the canonical path
- no fake/providerless assistant response bypass
---
<!-- event: review author: yoi-reviewer-00001KW7726H9-launch-unify at: 2026-06-28T17:51:13Z status: request_changes -->
## Review: request changes
request_changes
読み取り専用で Ticket `00001KW7726H9`、`artifacts/api-field-audit.md`、target commit `14bb4934a6374eea64591035e5342088ab0ccd09` の実装を確認した。重い検証コマンドは再実行していない。`git diff --check HEAD^..HEAD` は追加出力なし。
## Blockers
### 1. 永続化要件の `execution binding identity` と stale execution mapping 診断が未実装
Ticket の保存・再起動要件では、Worker 作成時に `execution binding identity` を保存し、再起動後に live execution handle を復元できない場合は stale execution mapping として typed diagnostic にする必要がある。
しかし実装では、永続化 record に execution binding 情報が保存されていない。
- `crates/worker-runtime/src/runtime.rs:1467-1477`
- `PersistedWorkerRecord``worker_ref`, `worker_id`, `status`, `request`, `transcript` などは保存されているが、`WorkerExecutionStatus` / binding id / execution handle identity が落ちている。
- `crates/worker-runtime/src/runtime.rs:978-1010`
- restore 時に全 Worker が `WorkerExecutionStatus::unconnected()` / `execution_handle: None` にされる。
- その際、`diagnostics` は persisted diagnostics をそのまま引き継ぐだけで、stale execution mapping の diagnostic を追加していない。
- `Runtime::diagnostics()``state.diagnostics.clone()` を返すのみで、restore された unconnected Worker を typed diagnostic として表面化しない。
これにより、次の意図を満たしていない。
- Worker 作成時に execution binding identity を保存する。
- 保存 identity は権限値ではなく stale execution mapping / reconnect の参照として扱う。
- 再起動後に live execution handle を復元できない場合は stale execution mapping として診断する。
- missing config / execution binding / history を typed diagnostic にし、黙って別 Worker を作らない。
現状は「黙って unconnected に戻す」挙動なので、Runtime worker creation の永続性・復元診断要件として blocker。
### 2. 初期 input で `System` を受け入れて transcript に保存できる
Ticket / API audit の方針では、Browser launch の initial input はユーザー投入分であり、launch/create 時に System message を作らない設計だった。System instruction / role prompt は ConfigBundle/Profile 側に寄せる意図である。
しかし現在の実装では、CreateWorkerRequest の `initial_input``WorkerInput` のままなので `WorkerInputKind::System` を受け取れる。
- `crates/worker-runtime/src/catalog.rs`
- `CreateWorkerRequest { config_bundle_ref, initial_input: Option<WorkerInput> }`
- `crates/worker-runtime/src/runtime.rs:261-265`
- `WorkerInputKind::System => TranscriptRole::System` として initial transcript に保存される。
- `crates/workspace-server/src/hosts.rs`
- Browser-facing `WorkerSpawnRequest.initial_input``EmbeddedWorkerInput` 経由で system/user を表現できる形になっている。
これにより、Browser/API caller が Worker 作成時に system transcript を注入できるため、Ticket の「Browser launch semantics と Runtime CreateWorkerRequest を分離し、initial input はユーザー投入分に限定する」意図と矛盾する。少なくとも create / launch initial input では User-only 型にするか、Runtime boundary で `System` を typed rejection する必要がある。
## 確認できた良い点
- Runtime `CreateWorkerRequest` から workspace / cwd / tool scope / config store / secret / socket / path 由来の大きな launch fields は削られ、基本的に `config_bundle_ref` + `initial_input` に寄っている。
- ConfigBundle missing / digest mismatch / profile mismatch は `RuntimeError` と diagnostic で扱う方向になっている。
- execution backend 不在時は providerless / fake Worker を返さず、create が失敗する経路になっている。
- embedded Workspace Server 側の spawn は ConfigBundle を Runtime に sync してから canonical Runtime create に寄せている。
---
<!-- event: implementation_report author: yoi-coder-00001KW7726H9-launch-unify at: 2026-06-28T18:12:02Z -->
## Implementation report
Change-response implementation report:
Reviewer blockers を修正し、追加 commit を作成済み。Ticket ファイル編集、merge、close、cleanup は未実施。
Commit:
- `c29d10b67bfff1f4a7a1b2742ec05fe63b80c054 runtime: persist execution binding projection`
変更内容:
- Execution binding identity の永続化
- `WorkerExecutionBindingIdentity { backend_id }` を追加。
- raw handle / secret / path / socket を含まない非権威 durable projection として保存。
- Worker persisted record に `WorkerExecutionStatus` を保存し、execution status / binding projection が restore 後に消えないようにした。
- stale execution mapping diagnostic
- fs-store restore 時、persisted execution binding が存在するが live execution handle を復元できない場合に Worker execution status を `stale` として投影。
- typed diagnostic `worker_execution_mapping_stale` を追加。
- stale projection は input-capable と誤認されないよう `WorkerExecutionBackendKind::Stale` として扱う。
- System initial input の拒否
- `CreateWorkerRequest.initial_input``WorkerInputKind::System` が来た場合、Runtime boundary で `RuntimeError::InvalidInitialInputKind` として拒否。
- System / role instructions は Profile / ConfigBundle に属し、launch/create initial input として transcript に保存されないようにした。
- Browser-facing embedded launch でも typed diagnostic `embedded_worker_initial_input_kind_invalid` として返す。
- Tests
- System initial input が reject され、Worker / WorkerCreated event が persisted されないことを確認。
- fs-store restore 後に stale execution mapping diagnostic が出ること、binding identity が raw authority ではなく backend id projection として残ることを確認。
- embedded runtime launch の System initial input reject を確認。
Validation:
- `cargo fmt`: success
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo test -p yoi-workspace-server`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `cargo test -p worker-runtime --features fs-store,ws-server`: success
- `nix build .#yoi`: success
Residual risks / review focus:
- restore 後の execution binding は非権威 projection であり、live handle は復元しない。再接続 / reattach 実処理は別設計範囲。
- stale Worker は binding identity と diagnostic を保持するが input-capable とは扱わないため、UI/API 側では stale diagnostic を見て明示的な再接続 / 再作成導線を出す必要がある。
---
<!-- event: review author: yoi-reviewer-00001KW7726H9-launch-unify at: 2026-06-28T18:17:07Z status: request_changes -->
## Review: request changes
request_changes
読み取り専用で再レビューした。前回指摘した Runtime 本体側の 2 点は概ね修正されているが、元の受け入れ条件に対する blocker が 1 件残っている。
## Blocker
### Remote Runtime projection が stale / unconnected Worker を input-capable として公開する
Runtime fs-store restore 側では、persisted execution binding があるが live handle が戻らない場合に `WorkerExecutionBackendKind::Stale``worker_execution_mapping_stale` diagnostic を出す実装になっている。
- `crates/worker-runtime/src/runtime.rs:993-1008`
- persisted binding + `Connected` を restore 時に `Stale` 化し、`worker_execution_mapping_stale` diagnostic を追加。
- `crates/worker-runtime/src/execution.rs:18-23`, `148-156`, `173-181`
- `Stale` backend kind と durable binding projection が追加されている。
しかし Workspace Server の remote Runtime projection では、Runtime HTTP から返る `WorkerSummary` / `WorkerDetail``execution` を無視して、常に `can_accept_input: true` にしている。
- `crates/workspace-server/src/hosts.rs:1683-1704`
- remote `map_worker_summary()``summary.execution` を見ずに `can_accept_input: true`
- `crates/workspace-server/src/hosts.rs:1715-1736`
- remote `map_worker_detail()``detail.execution` を見ずに `can_accept_input: true`
- 対照的に embedded projection は `can_accept_embedded_input()``status == Running` かつ `execution.backend == Connected` などを確認している(`hosts.rs:981-990`, `993-1015`, `1026-1048`)。
このため、remote-facing Worker creation / listing では、Runtime 側で stale と診断された Worker でも Workspace API 上は input-capable と見えてしまう。Ticket の以下の要件を満たしていない。
- `input-capable Worker が execution backend 未接続になる経路がない`
- `Stale is not input-capable`
- `embedded Worker / Workspace Companion / remote-facing Worker creation が同じ作成手順を使う`
remote projection でも Runtime response の `execution.backend`, `run_state`, `status`, `last_result` を使って embedded と同等に `can_accept_input` / `can_stop` を計算する必要がある。
## 前回 blocker の確認状況
- Execution binding identity persistence は Runtime record に `execution` が保存されるようになっており、`WorkerExecutionBindingIdentity { backend_id }` も raw handle / socket / path / secret を保持していない。
- restore 時の stale diagnostic は追加されている。
- `CreateWorkerRequest.initial_input``System` は Runtime で `InvalidInitialInputKind` として拒否され、transcript には user role のみで作成される経路になっている。
- create は execution backend 未接続なら拒否し、fake/providerless response を返さない方向である。
重い validationcargo / nixは再実行していない。reported validation は未再現。
---
<!-- event: implementation_report author: yoi-coder-00001KW7726H9-launch-unify at: 2026-06-28T18:35:12Z -->
## Implementation report
Change-response implementation report:
Reviewer の remote Runtime projection blocker を修正し、追加 commit を作成済み。Ticket ファイル編集、merge、close、cleanup は未実施。
Commit:
- `ba7f9d2ee83a946820cc234e847b6531b4a141f3 workspace: respect remote execution projection`
Note:
- Coder output initially reported a different SHA prefix, but `git rev-parse HEAD` confirmed the actual commit above.
変更内容:
- `crates/workspace-server/src/hosts.rs` の remote Runtime projection で、Runtime HTTP の `WorkerSummary` / `WorkerDetail.execution` を反映するように修正。
- remote projection の `can_accept_input` / `can_stop` は embedded projection と同様に以下を確認する。
- Worker status が `running`
- `execution.backend == connected`
- input は `execution.run_state == idle`
- `rejected` / `errored` / `unconnected` / `stopped` などの実行状態は操作不可
- `last_result``rejected` / `errored` / `unsupported` の場合は操作不可
- remote Worker の表示 status も execution status を反映し、`stale` / `unconnected` / `rejected` / `errored` を投影する。
- remote Runtime の stale / unconnected / rejected / errored projection が input-capable / stoppable にならないことを確認する tests を追加。
- 既存 remote connected projection test に、connected idle の場合だけ input / stop capable になる確認を追加。
Validation:
- `cargo fmt`: success
- `cargo test -p yoi-workspace-server`: success
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi`: success
Residual risks / review focus:
- remote Runtime の `execution` projection を信頼して UI/API capability を決めるため、remote 側が古い schema や不正な execution state を返す場合は deserialize / request failure として扱われる。
- stale / unconnected は明示的に input-capable ではなくなるため、既存 UI が「remote worker は常に入力可」と仮定している場合は表示 / 操作導線の調整が必要になる可能性がある。
---
<!-- event: review author: yoi-reviewer-00001KW7726H9-launch-unify at: 2026-06-28T18:37:36Z status: approve -->
## Review: approve
approve
読み取り専用で再レビューした。HEAD は `ba7f9d2ee83a946820cc234e847b6531b4a141f3 workspace: respect remote execution projection`。`git diff --check c29d10b..ba7f9d2` は追加出力なし。cargo / nix 系の reported validation は再実行していない。
## 確認結果
前回 blocker だった remote Runtime projection は解消されている。
- `crates/workspace-server/src/hosts.rs:1670-1693`
- remote `map_worker_summary()` が Runtime HTTP response の `summary.execution` を参照し、`can_accept_input` / `can_stop` を計算するようになっている。
- `crates/workspace-server/src/hosts.rs:1703-1726`
- remote `map_worker_detail()``detail.execution` を使っている。
- `crates/workspace-server/src/hosts.rs:2106-2134`
- `runtime_worker_can_accept_input()``Running` + `Connected` + `Idle` + rejected / errored / unsupported last_result なしを要求。
- `runtime_worker_can_stop()``Running` + `Connected` を要求し、stopped / rejected / errored / unconnected run_state と blocking last_result を拒否。
- `crates/workspace-server/src/hosts.rs:2155-2175`
- display status も `Stale` / `Unconnected` / `Rejected` / `Errored` を反映する。
- `crates/workspace-server/src/hosts.rs:3384-3475`
- stale / unconnected / rejected / errored の remote projection が input-capable / stoppable にならない test が追加されている。
- 既存 remote positive path でも connected idle worker が input / stop capable であることが確認されている。
これにより、`stale` / execution backend 未接続 Worker が Workspace API 上で input-capable として見える問題は解消されている。
## 総合判断
前回までの blockerexecution binding projection persistence、stale diagnostic、System initial input rejection、remote stale projectionについて、現在の差分で受け入れ条件を満たしていると判断する。現時点で concrete blocker はない。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T18:38:41Z -->
## Implementation report
Merge / validation report:
Implementation branch was integrated into the orchestration branch after reviewer approval.
Integrated commits:
- `14bb4934a6374eea64591035e5342088ab0ccd09 runtime: unify worker creation path`
- `c29d10b67bfff1f4a7a1b2742ec05fe63b80c054 runtime: persist execution binding projection`
- `ba7f9d2ee83a946820cc234e847b6531b4a141f3 workspace: respect remote execution projection`
Merge commit:
- `bdb339fa merge: runtime worker launch unification`
Validation in orchestration worktree:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo test -p worker-runtime --features "fs-store ws-server"`: success
- `cargo test -p yoi-workspace-server`: success (`36 passed`)
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Outcome:
- Runtime Worker creation is unified around canonical `ConfigBundleRef + ExecutionBackend` create semantics.
- Browser-facing launch semantics are separated from Runtime `CreateWorkerRequest`.
- Runtime create request no longer carries raw workspace/cwd/tool-scope/config-store/secret/socket/path semantics.
- ConfigBundle missing/digest/profile mismatch and execution backend missing/provider failures are typed diagnostics/errors.
- Input-capable Workers require execution backend connection.
- Execution binding identity is persisted only as non-authority projection and restore without live handle surfaces stale execution mapping diagnostics.
- System initial input is rejected at Runtime boundary and not persisted as launch-created system transcript.
- Remote Runtime projection respects execution state so stale/unconnected/rejected/errored Workers are not input-capable.
- fake/providerless response bypass was not introduced.
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-28T18:38:48Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
## State changed
Reviewer approval後に implementation branch を orchestration branch へ merge し、要求された validation が成功したため done に遷移する。
Validation:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo test -p worker-runtime --features "fs-store ws-server"`: success
- `cargo test -p yoi-workspace-server`: success (`36 passed`)
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
---
<!-- event: state_changed author: hare at: 2026-06-28T18:39:03Z from: done to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-28T18:39:03Z status: closed -->
## 完了
Runtime Worker 起動経路を canonical `ConfigBundleRef + ExecutionBackend` 経路へ一本化し、reviewer approval 後に orchestration branch へ merge した。
実装内容:
- Runtime `CreateWorkerRequest``config_bundle_ref` と user-only initial input 中心の内部作成契約へ整理。
- Browser-facing launch semantics と Runtime worker creation request を分離。
- raw workspace / cwd / tool scope / config store / secret / socket / path 類を Runtime create request に含めない境界にした。
- ConfigBundle missing / digest mismatch / profile mismatch を typed error / diagnostic として扱う。
- execution backend 未接続では Worker 作成を拒否し、input-capable Worker が backend 未接続になる経路を塞いだ。
- create 成功時のみ catalog / transcript / event を永続化し、spawn / initial input dispatch rejection は rollback。
- execution binding identity を raw handle / secret / path / socket を含まない non-authority projection として永続化。
- restore 後に live handle がない persisted execution mapping は `stale` として diagnostic `worker_execution_mapping_stale` を出す。
- System initial input を Runtime boundary で拒否し、launch/create が system transcript を注入できないようにした。
- Workspace embedded / Companion / remote-facing creation を canonical Runtime create path に寄せた。
- Remote Runtime projection でも execution status を見て `can_accept_input` / `can_stop` を計算し、stale / unconnected / rejected / errored Workers を input-capable として出さないようにした。
- fake/providerless response bypass は導入していない。
Integrated commits:
- `14bb4934a6374eea64591035e5342088ab0ccd09 runtime: unify worker creation path`
- `c29d10b67bfff1f4a7a1b2742ec05fe63b80c054 runtime: persist execution binding projection`
- `ba7f9d2ee83a946820cc234e847b6531b4a141f3 workspace: respect remote execution projection`
- merge: `bdb339fa merge: runtime worker launch unification`
Validation:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo test -p worker-runtime --features "fs-store ws-server"`: success
- `cargo test -p yoi-workspace-server`: success (`36 passed`)
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Operational note:
- User instructed not to use `StopPod` for now after repeated stop/hang symptoms. Cleanup will skip role-Pod shutdown and remove only child implementation worktree / branch.
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T18:39:24Z -->
## Implementation report
Cleanup report:
- User instructed not to use `StopPod` for now after repeated stop/hang symptoms. Role Pod shutdown was intentionally skipped.
- Child implementation worktree was removed:
- `/home/hare/Projects/yoi/.worktree/00001KW7726H9-runtime-worker-launch-unification`
- Child implementation branch was removed:
- `work/00001KW7726H9-runtime-worker-launch-unification`
- No root/original workspace cleanup was performed.
- Orchestration worktree is clean after worktree/branch cleanup.
Routing note:
- Tickets that were queued behind `00001KW7726H9` can now be re-evaluated.
---

View File

@ -1 +1,2 @@
{"id":"orch-plan-20260628-165114-1","ticket_id":"00001KW7835H0","kind":"blocked_by","related_ticket":"00001KW7726H9","note":"`00001KW7835H0` depends on `00001KW7726H9`; old Pod registry/store removal should follow the canonical Runtime Worker creation authority decision. `00001KW7726H9` is currently inprogress, so this Ticket remains queued and must not start a child worktree/Pod until that dependency is merged/validated/closed or an explicit replan changes the dependency.","author":"yoi-orchestrator","at":"2026-06-28T16:51:14Z"}
{"id":"orch-plan-20260628-191721-2","ticket_id":"00001KW7835H0","kind":"accepted_plan","note":"Dependency `00001KW7726H9` is closed and related fs-store persistence Ticket `00001KW76E8EG` is also closed. No current inprogress Tickets and orchestration worktree is clean, so this final queued Ticket is unblocked.","accepted_plan":{"summary":"`00001KW7726H9` と `00001KW76E8EG` が closed になったため、旧 `pod-registry` / `pod-store` crate を削除し、必要な Worker metadata/session persistence を Worker/Runtime/session-store 側へ移す。`crates/pod` は既に存在しないため不在確認と再導入防止を行う。Runtime Worker identity / creation / persistence は worker-runtime fs-store と execution backend mapping を正とし、旧 Pod registry/store/path/socket/name を active authority として残さない。","branch":"work/00001KW7835H0-old-pod-crate-cleanup","worktree":"/home/hare/Projects/yoi/.worktree/00001KW7835H0-old-pod-crate-cleanup","role_plan":"Orchestrator が dedicated child worktree を作成し、coder Worker に旧 Pod crate 削除・依存移管に必要な focused write scope (`crates/pod-registry`, `crates/pod-store`, `crates/worker`, `crates/tui`, `crates/yoi`, `crates/worker-runtime`, `crates/session-store`, `crates/manifest`, root Cargo/package files など) を委譲する。reviewer Worker は read-only で dependency audit、crate removal、metadata/session persistence 移管、Runtime authority invariant、UI/CLI/prompt/test wording cleanup、validation を確認する。merge/validation/done/cleanup は Orchestrator が行う。現時点では `StopPod` は使用せず、cleanup は child worktree/branch のみ行う。"},"author":"yoi-orchestrator","at":"2026-06-28T19:17:21Z"}

View File

@ -1,8 +1,8 @@
---
title: '旧Pod関連クレートを削除しWorker/Runtime storeへ整理する'
state: 'queued'
state: 'closed'
created_at: '2026-06-28T13:53:21Z'
updated_at: '2026-06-28T16:51:22Z'
updated_at: '2026-06-28T20:10:50Z'
assignee: null
queued_by: 'workspace-panel'
queued_at: '2026-06-28T16:51:00Z'

View File

@ -0,0 +1,34 @@
旧 Pod 関連 crate を削除し、Worker/Runtime/session-store 境界へ整理した実装を reviewer approval 後に orchestration branch へ merge した。
実装内容:
- `crates/pod-registry``crates/pod-store` を workspace / package / lock / source から削除。
- `crates/pod` は引き続き absent。
- `pod-store` 相当の Worker metadata persistence を `session-store::worker_metadata` へ移管。
- Worker metadata root を active Worker data root 側へ整理。
- `pod-registry` 相当の scope allocation 実装を `worker::runtime::worker_allocation` へ移管し、Runtime Worker identity / creation / durable persistence authority ではないことを明記。
- active CLI / tool / test artifact output を旧 Pod authority wording から Worker / allocation / metadata wording へ変更。
- TUI / yoi / worker の active old crate dependency を削除。
- E2E fixture の Worker metadata root を active `manifest::paths::data_dir()` default resolution と整合する `$HOME/.yoi/workers` に変更。
- Runtime Worker identity / creation / persistence authority は `worker-runtime` fs-store + execution backend mapping のまま維持。
- old Pod registry/store/socket/name を active authority として残す互換 alias は導入していない。
Integrated commits:
- `17a9488a4aa0a3dc83be2d3360b6ffd8ffcaeb5a refactor: remove old pod crates`
- `c46e880b fix: finish worker wording cleanup`
- `fdd902d5 fix: align e2e worker metadata root`
- merge: `83d433bf merge: old pod crate cleanup`
Validation:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo test -p yoi-workspace-server`: success
- `cargo test -p worker`: success
- `cargo test -p session-store`: success
- `cargo check -p yoi`: success
- `cargo check -p yoi-e2e`: success
- `cd web/workspace && deno task check`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Operational note:
- User instructed not to use `StopPod` for now after repeated stop/hang symptoms. Cleanup will skip role-Pod shutdown and remove only child implementation worktree / branch.

View File

@ -112,3 +112,479 @@ Next routing condition:
- Re-evaluate this Ticket after `00001KW7726H9` is merged, validated, and closed, or after an explicit replan changes the dependency.
---
<!-- event: decision author: yoi-orchestrator at: 2026-06-28T19:17:30Z -->
## Decision
Routing decision:
The previously blocking dependency `00001KW7726H9` is now merged, validated, closed, and cleaned up. Related fs-store persistence Ticket `00001KW76E8EG` is also merged, validated, closed, and cleaned up. Current workspace state was inspected before implementation side effects.
Findings:
- Ticket state: `queued`
- Blocking dependency: `00001KW7726H9` is closed.
- Related persistence Ticket: `00001KW76E8EG` is closed.
- Current `inprogress` Tickets: 0
- Orchestration worktree: clean
Decision:
- Accept this Ticket for implementation and transition `queued -> inprogress` before child worktree / Pod side effects.
Plan:
- Worktree: `/home/hare/Projects/yoi/.worktree/00001KW7835H0-old-pod-crate-cleanup`
- Branch: `work/00001KW7835H0-old-pod-crate-cleanup`
- Coder Worker will remove old `pod-registry` / `pod-store` crates and migrate any remaining authority/persistence responsibilities to Worker/Runtime/session-store boundaries.
- Reviewer Worker will verify crate removal, dependency graph cleanup, Runtime authority invariants, stale compatibility avoidance, Browser/UI/CLI/prompt/test wording, and validation evidence.
Operational note:
- User instructed not to use `StopPod` for now after repeated stop/hang symptoms. Cleanup will skip role-Pod shutdown and remove only child worktree/branch unless explicitly reauthorized.
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-28T19:17:36Z from: queued to: inprogress reason: dependencies_closed_and_queue_continues field: state -->
## State changed
Dependencies are closed, no Ticket is currently inprogress, and the orchestration worktree is clean. Accepting implementation before child worktree / role Pod side effects.
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T19:18:26Z -->
## Implementation report
Implementation kickoff:
- Worktree created: `/home/hare/Projects/yoi/.worktree/00001KW7835H0-old-pod-crate-cleanup`
- Branch: `work/00001KW7835H0-old-pod-crate-cleanup`
- Coder Worker spawned: `yoi-coder-00001KW7835H0-pod-cleanup`
Scope notes:
- Coder write scope covers the old crate directories `crates/pod-registry`, `crates/pod-store`, affected active crates (`crates/worker`, `crates/tui`, `crates/yoi`, `crates/worker-runtime`, `crates/session-store`, `crates/manifest`), prompt/profile/web workspace paths, and root Cargo/package files.
- `crates/pod` is already absent; Coder is instructed to verify absence and prevent reintroduction.
- Coder is instructed not to modify Ticket records and not to operate in root/orchestration worktrees.
Implementation focus:
- remove `crates/pod-registry` and `crates/pod-store` from workspace
- migrate/rename any remaining Worker metadata/session persistence to Worker/Runtime/session-store boundaries without active Pod authority naming
- remove old pod-registry path APIs and old Pod metadata inventory/cleanup dependencies from worker/tui/yoi where active
- ensure Runtime Worker identity / creation / persistence remains `worker-runtime` fs-store + execution backend mapping authority
- do not use old Pod registry/store/socket/name as active authority
- keep historical docs only if clearly historical
- validate dependency graph and run targeted tests/checks
---
<!-- event: implementation_report author: yoi-coder-00001KW7835H0-pod-cleanup at: 2026-06-28T19:45:57Z -->
## Implementation report
Implementation report:
実装完了。Ticket ファイル編集、merge、close、cleanup は未実施。
Commit:
- `17a9488a4aa0a3dc83be2d3360b6ffd8ffcaeb5a refactor: remove old pod crates`
実装内容:
- `crates/pod-registry` / `crates/pod-store` を workspace から削除。
- `Cargo.toml` / `Cargo.lock` / 各 crate dependency / `package.nix` から旧 crate 参照を除去。
- `crates/pod` も引き続き存在しないことを確認。
- `pod-registry` / `pod-store` / `crates/pod` 系の active 参照が残っていないことを `rg` で確認。
- `pod-store` の Worker metadata 永続化を `session-store` 境界へ移動。
- `session_store::worker_metadata` として `FsWorkerStore`, `WorkerMetadataStore`, `CombinedStore` 等を提供。
- active storage naming を Worker metadata に寄せ、metadata root も `data_dir/workers` 側へ変更。
- `pod-registry` の active crate 依存を削除。
- Worker 内部の `runtime::worker_allocation` モジュールへ、scope allocation 用の実装として分類・移設。
- module comment で「Runtime Worker identity / creation / durable persistence の authority ではない」ことを明記。
- manifest path API は `worker_allocation_path` / `worker_socket_path` 名へ置換。
- `tui` / `yoi` / `worker` の旧 crate 依存を置換。
- TUI の live worker inventory は旧 `pod-registry` crate に依存せず、renamed worker allocation table を read-only に読む形へ変更。
- 読み取り時は `fs4` の shared lock を使い、writer との競合で partial read しにくい形に修正。
- `package.nix``cargoHash` を更新。
依存監査サマリ:
- `crates/pod`: absent
- `crates/pod-registry`: deleted
- `crates/pod-store`: deleted
- workspace/package/lock/source 内の active `pod-registry` / `pod-store` / `crates/pod` 参照: なし
- Runtime Worker identity/creation/persistence authority は `worker-runtime` fs-store + execution backend mapping のまま維持し、旧 registry/store/socket/name を authority として使わない構成に整理。
Validation:
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo test -p yoi-workspace-server`: success
- `cargo test -p worker`: success
- `cargo test -p session-store`: success
- `cargo check -p yoi`: success
- `cd web/workspace && deno task check`: success
- `git diff --check`: success
- `git diff --cached --check`: success
- `nix build .#yoi`: success
Risks / notes:
- Worker metadata root を `data_dir/pods` 由来から `data_dir/workers` 側へ寄せたため、既存ローカル metadata の自動移行は入れていない。Ticket 方針どおり、旧 Pod authority naming 互換を増やさない実装。
- Historical docs は今回の active dependency cleanup の対象外だが、active crate/path/dependency 参照は除去済み。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T19:46:27Z -->
## Implementation report
Review kickoff:
Reviewer Worker spawned for `00001KW7835H0` implementation.
- Reviewer Worker: `yoi-reviewer-00001KW7835H0-pod-cleanup`
- Review target commit: `17a9488a4aa0a3dc83be2d3360b6ffd8ffcaeb5a refactor: remove old pod crates`
- Review mode: read-only implementation worktree review
Review focus:
- `crates/pod-registry` and `crates/pod-store` are removed from workspace / Cargo / package / lock / active source references
- `crates/pod` remains absent and is not reintroduced
- needed Worker metadata/session persistence moved to Worker/Runtime/session-store boundaries without active Pod authority naming
- old pod-registry APIs are not active authority
- Runtime Worker identity / creation / persistence remains `worker-runtime` fs-store + execution backend mapping
- UI/CLI/TUI active wording and tests compile
- no stale compatibility alias preserves old Pod store authority
- validation evidence is plausible and sufficient
---
<!-- event: review author: yoi-reviewer-00001KW7835H0-pod-cleanup at: 2026-06-28T19:51:02Z status: request_changes -->
## Review: request changes
request_changes
対象 commit `17a9488a4aa0a3dc83be2d3360b6ffd8ffcaeb5a` を read-only で確認した。worktree は clean、`git diff --check HEAD^ HEAD` は成功。重い `cargo` / `deno` / `nix` は reviewer では再実行せず、報告済み validation は diff/code 上で plausibility を確認した。
## 満たしている点
- `crates/pod` / `crates/pod-registry` / `crates/pod-store` のディレクトリは存在しない。
- `Cargo.toml` workspace members/default-members/dependencies、active crate `Cargo.toml`、`Cargo.lock`、`package.nix` から `pod-registry` / `pod-store` dependency は除去済み。
- `manifest::paths::pod_registry_path` / `pod_registry::...` / `pod_store::...` 参照は active source から消えている。
- `pod-store` 相当の `WorkerMetadataStore` / `FsWorkerStore` / `CombinedStore``session-store::worker_metadata` へ移され、`data_dir/workers` の Worker metadata と session log store に分離されている。
- `worker-runtime` 側は `RuntimeId + WorkerId` を authority とする fs-store / execution backend mapping が残っており、旧 pod path/socket/session path を authority にしない設計コメント・境界も確認した。
## Blockers
1. **active CLI output に旧 `pod` 名が残っている。**
`crates/yoi/src/worker_cleanup_cli.rs:242` / `253``yoi worker delete` の refusal 出力が `pod: {}` になっている。これは Ticket の「TUI / yoi CLI / worker cleanup CLI から旧 Pod metadata inventory / cleanup 依存を削除」「Active UI / CLI / prompt / test output に旧 Pod concept が正規 concept として残らない」に反する。`worker:` に直す必要がある。
2. **LLM/tool-visible な active Worker allocation error が旧 Pod authority wording のまま。**
`crates/worker/src/runtime/worker_allocation/error.rs:14,16,29,32` に以下が残っている:
- `pod name ... is already registered`
- `conflicts with pod ...`
- `pod ... is not registered`
- `session ... is already held by pod ...`
これらは `crates/worker/src/spawn/tool.rs:867-875``ToolError``e.to_string()` として流れるため、単なる内部コメントではなく active tool/user-facing error。旧 `pod-registry` crate は消えているが、移管先の active scope allocation path が旧 Pod 名で authority/error を表現しており、Ticket intent に対して未完了。
3. **active e2e harness artifact に旧 pod registry / metadata path が残っている。**
`tests/e2e/src/lib.rs:1624-1625``tested_yoi_pod_registry: ... pods.json``fixture_pod_metadata_root: ... pods` を書き出している。`tests/e2e` は workspace member なので、historical docs/report ではなく active test source/output と見なされる。現 Runtime/Worker path へ更新するか、legacy/historical fixture として明確に分類・隔離する必要がある。
上記を直したうえで、報告済み validation 一式(少なくとも `cargo check -p yoi`, `cargo test -p worker`, `cargo test -p worker-runtime --features ws-server`, `cargo test -p yoi-workspace-server`, `deno task check`, `git diff --check`, `nix build .#yoi --no-link`)を再確認すること。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T19:51:35Z -->
## Implementation report
Change-request routing:
Reviewer requested changes for active wording/test artifact leftovers. The original Coder scope did not include `tests/e2e`, so a focused fix Coder Worker was spawned with additional write scope for the specific blocker areas.
- Fix Coder Worker: `yoi-coder-00001KW7835H0-pod-fix`
- Worktree: `/home/hare/Projects/yoi/.worktree/00001KW7835H0-old-pod-crate-cleanup`
Requested fixes:
- change active `yoi worker delete` refusal output from `pod:` to `worker:`
- rename active worker allocation tool/user-facing errors away from old Pod authority wording
- update active `tests/e2e` harness artifact names/paths away from old pod registry / pod metadata wording or clearly isolate as legacy if required
- run focused validation and commit fixes
---
<!-- event: implementation_report author: yoi-coder-00001KW7835H0-pod-fix at: 2026-06-28T19:55:08Z -->
## Implementation report
Change-response implementation report:
Reviewer blockers を修正し、追加 commit を作成済み。Ticket ファイル編集、merge、close、cleanup は未実施。
Commit:
- `c46e880b fix: finish worker wording cleanup`
変更内容:
- `crates/yoi/src/worker_cleanup_cli.rs`
- `yoi worker delete` の refusal 出力を `pod:` から `worker:` に修正。
- `crates/worker/src/runtime/worker_allocation/error.rs`
- active/user-facing error 文言を旧 Pod 表現から Worker/allocation 表現に修正。
- `pod name`, `conflicts with pod`, `pod ... not registered`, `held by pod` を置換。
- `tests/e2e/src/lib.rs`
- E2E artifact 名と fixture metadata path を Worker/allocation/metadata 表現に更新。
- `tested_yoi_pod_registry` -> `tested_yoi_worker_allocation`
- `fixture_pod_metadata_root` -> `fixture_worker_metadata_root`
- fixture path `yoi/pods/...` -> `yoi/workers/...`
- helper 名を `write_blocking_worker_metadata` に更新。
Validation:
- `cargo fmt`: success
- `cargo test -p worker`: success
- `cargo check -p yoi`: success
- `cargo check -p yoi-e2e`: success
- `git diff --check`: success
- 対象旧表現の `rg` 確認: success
Final state:
- child worktree is clean.
---
<!-- event: review author: yoi-reviewer-00001KW7835H0-pod-cleanup at: 2026-06-28T20:00:09Z status: request_changes -->
## Review: request changes
request_changes
再レビューは read-only で実施した。`c46e880b` は確認済みで、worktree は clean、`git diff --check HEAD^ HEAD` は成功。重い `cargo` / `deno` / `nix` は read-only 指示のため再実行せず、報告 validation はコード上の整合性を確認した。
前回指摘分のうち、以下は修正済み:
- `crates/yoi/src/worker_cleanup_cli.rs` の active refusal output は `pod:` ではなく `worker:` になっている。
- `crates/worker/src/runtime/worker_allocation/error.rs` の user-facing error は Pod wording から Worker/allocation wording に変更されている。
- `tests/e2e/src/lib.rs` の artifact key / helper 名は `tested_yoi_worker_allocation` / `fixture_worker_metadata_root` / `write_blocking_worker_metadata` に変更されている。
- `crates/pod` / `crates/pod-registry` / `crates/pod-store` の crate directory / workspace dependency / lock / package 参照は残っていない。
## Blocker
### E2E fixture の Worker metadata root が active store と一致していない
`tests/e2e/src/lib.rs` は現在も Worker metadata を `XDG_DATA_HOME` 側へ書いている。
- `tests/e2e/src/lib.rs:1353-1354`
- `write_blocking_worker_metadata(&fixture.xdg_data_home, "workspace")?;`
- `write_blocking_worker_metadata(&fixture.xdg_data_home, "workspace-orchestrator")?;`
- `tests/e2e/src/lib.rs:1625`
- `"fixture_worker_metadata_root": self.xdg_data_home.join("yoi").join("workers")`
- `tests/e2e/src/lib.rs:2000-2003`
- `data_home.join("yoi").join("workers").join(worker_name)`
しかし active Worker metadata store は `manifest::paths::data_dir()` ベース:
- `crates/manifest/src/paths.rs:41-46`
- `data_dir()``YOI_DATA_DIR` / `YOI_HOME` / `HOME` を見る。`XDG_DATA_HOME` は使わない。
- `crates/worker/src/entrypoint.rs:487-494`
- worker metadata dir は `paths::data_dir().join("workers")`
- `crates/yoi/src/worker_cleanup_cli.rs:209-237`
- cleanup CLI も `paths::data_dir().join("workers")`
E2E harness は `HOME``XDG_DATA_HOME` を分けて渡しており、`YOI_DATA_DIR` は設定していない。そのため実際の active metadata root は fixture 上では概ね `$HOME/.yoi/workers` だが、現在の fixture/helper/artifact は `$XDG_DATA_HOME/yoi/workers` を指している。
これは表面上の naming は直っているが、移行後の Worker/session-store 境界と一致していないため、E2E fixture が正しい Worker metadata store をブロック・記録できない。Ticket の「必要な Worker metadata/session persistence を Worker/Runtime/session-store boundaries へ移す」「tests が旧 Pod metadata inventory に依存しない」に対して未完了。
修正案:
- `write_blocking_worker_metadata``fixture.home` または解決済み `data_dir` を渡し、`$HOME/.yoi/workers` に書く。
- または E2E subprocess に `YOI_DATA_DIR` を明示し、helper / artifact / subprocess env が同じ data root を使うように揃える。
- `fixture_worker_metadata_root` も同じ resolved active path を記録する。
補足: `YOI_POD_RUNTIME_COMMAND` の残存は既存設計上の dev executable-path override として扱い、今回の blocker にはしていない。
---
<!-- event: implementation_report author: yoi-coder-00001KW7835H0-pod-fix at: 2026-06-28T20:02:28Z -->
## Implementation report
Change-response implementation report:
Reviewer の E2E Worker metadata root blocker を修正し、追加 commit を作成済み。Ticket ファイル編集、merge、close、cleanup は未実施。
Commit:
- `fdd902d5 fix: align e2e worker metadata root`
変更内容:
- `tests/e2e/src/lib.rs`
- E2E fixture の Worker metadata helper を active code の `manifest::paths::data_dir()` の既定解決に合わせた。
- `XDG_DATA_HOME/yoi/workers` ではなく、fixture の `HOME` から解決される `$HOME/.yoi/workers` を使用。
- `fixture_worker_metadata_root` artifact も同じ `$HOME/.yoi/workers` を記録するよう修正。
- `active_worker_metadata_root(&home)` helper を追加し、書き込み先と artifact 記録を同一関数で揃えた。
Validation:
- `cargo fmt`: success
- `cargo check -p yoi-e2e`: success
- `git diff --check`: success
- `cargo test -p yoi-e2e`: success
- `cargo check -p yoi`: success
Final state:
- child worktree is clean.
---
<!-- event: review author: yoi-reviewer-00001KW7835H0-pod-cleanup at: 2026-06-28T20:04:44Z status: approve -->
## Review: approve
approve
read-only で再レビューした。対象 HEAD は `fdd902d5 fix: align e2e worker metadata root`、worktree は clean。
確認した点:
- 前回 blocker の E2E Worker metadata root は解消済み。
- `tests/e2e/src/lib.rs``active_worker_metadata_root(&fixture.home)` を使い、helper write path と artifact path が同じ `$HOME/.yoi/workers` を参照している。
- `write_blocking_worker_metadata``active_worker_metadata_root(home)` 経由であり、`manifest::paths::data_dir()` の default HOME 解決と整合している。
- `fixture_worker_metadata_root` artifact も同じ helper 経由で記録されている。
- 前回までの blocker 修正も維持されている。
- `yoi worker delete` refusal output は `worker:`
- `worker_allocation` の user-facing error は Pod wording ではなく Worker/allocation wording。
- E2E artifact/helper 名は `tested_yoi_worker_allocation` / `fixture_worker_metadata_root` / `write_blocking_worker_metadata`
- 旧 crate / authority 参照の除去を再確認。
- `crates/pod`, `crates/pod-registry`, `crates/pod-store` は不在。
- workspace / active dependency / lock / package への `pod-registry` / `pod-store` 参照は見当たらない。
- `pod_registry_path`, `pod_registry::`, `pod_store::` など旧 API authority 参照も active source では見当たらない。
- `git diff --check HEAD^ HEAD`: success。
- 報告 validation`cargo fmt`, `cargo check -p yoi-e2e`, `cargo test -p yoi-e2e`, `cargo check -p yoi`)は、今回の diff と import/path 整合性から plausible。read-only 指示のため cargo 系は再実行していない。
ブロッカーはない。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T20:10:03Z -->
## Implementation report
Merge / validation report:
Implementation branch was integrated into the orchestration branch after reviewer approval.
Integrated commits:
- `17a9488a4aa0a3dc83be2d3360b6ffd8ffcaeb5a refactor: remove old pod crates`
- `c46e880b fix: finish worker wording cleanup`
- `fdd902d5 fix: align e2e worker metadata root`
Merge commit:
- `83d433bf merge: old pod crate cleanup`
Validation in orchestration worktree:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo test -p yoi-workspace-server`: success
- `cargo test -p worker`: success
- `cargo test -p session-store`: success
- `cargo check -p yoi`: success
- `cargo check -p yoi-e2e`: success
- `cd web/workspace && deno task check`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Outcome:
- `crates/pod-registry` and `crates/pod-store` were removed from workspace/package/lock/source.
- `crates/pod` remains absent.
- Worker metadata store responsibility moved to `session-store::worker_metadata` under Worker naming.
- Runtime allocation implementation now lives under `worker::runtime::worker_allocation` and is documented as scope allocation, not Runtime Worker identity/creation/durable-persistence authority.
- Active CLI/tool/test output was renamed away from old Pod authority wording.
- E2E worker metadata fixture now uses the active `$HOME/.yoi/workers` data root resolution rather than `XDG_DATA_HOME/yoi/workers`.
- Runtime Worker identity / creation / persistence remains `worker-runtime` fs-store + execution backend mapping authority.
- No stale compatibility alias preserving old Pod store authority was introduced.
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-28T20:10:12Z from: inprogress to: done reason: implementation_merged_and_validated field: state -->
## State changed
Reviewer approval後に implementation branch を orchestration branch へ merge し、要求された validation が成功したため done に遷移する。
Validation:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo test -p yoi-workspace-server`: success
- `cargo test -p worker`: success
- `cargo test -p session-store`: success
- `cargo check -p yoi`: success
- `cargo check -p yoi-e2e`: success
- `cd web/workspace && deno task check`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
---
<!-- event: state_changed author: hare at: 2026-06-28T20:10:27Z from: done to: closed reason: closed field: state -->
## State changed
Ticket を closed にしました。
---
<!-- event: close author: hare at: 2026-06-28T20:10:27Z status: closed -->
## 完了
旧 Pod 関連 crate を削除し、Worker/Runtime/session-store 境界へ整理した実装を reviewer approval 後に orchestration branch へ merge した。
実装内容:
- `crates/pod-registry``crates/pod-store` を workspace / package / lock / source から削除。
- `crates/pod` は引き続き absent。
- `pod-store` 相当の Worker metadata persistence を `session-store::worker_metadata` へ移管。
- Worker metadata root を active Worker data root 側へ整理。
- `pod-registry` 相当の scope allocation 実装を `worker::runtime::worker_allocation` へ移管し、Runtime Worker identity / creation / durable persistence authority ではないことを明記。
- active CLI / tool / test artifact output を旧 Pod authority wording から Worker / allocation / metadata wording へ変更。
- TUI / yoi / worker の active old crate dependency を削除。
- E2E fixture の Worker metadata root を active `manifest::paths::data_dir()` default resolution と整合する `$HOME/.yoi/workers` に変更。
- Runtime Worker identity / creation / persistence authority は `worker-runtime` fs-store + execution backend mapping のまま維持。
- old Pod registry/store/socket/name を active authority として残す互換 alias は導入していない。
Integrated commits:
- `17a9488a4aa0a3dc83be2d3360b6ffd8ffcaeb5a refactor: remove old pod crates`
- `c46e880b fix: finish worker wording cleanup`
- `fdd902d5 fix: align e2e worker metadata root`
- merge: `83d433bf merge: old pod crate cleanup`
Validation:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime --features ws-server`: success
- `cargo test -p yoi-workspace-server`: success
- `cargo test -p worker`: success
- `cargo test -p session-store`: success
- `cargo check -p yoi`: success
- `cargo check -p yoi-e2e`: success
- `cd web/workspace && deno task check`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Operational note:
- User instructed not to use `StopPod` for now after repeated stop/hang symptoms. Cleanup will skip role-Pod shutdown and remove only child implementation worktree / branch.
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-28T20:10:50Z -->
## Implementation report
Cleanup report:
- User instructed not to use `StopPod` for now after repeated stop/hang symptoms. Role Pod shutdown was intentionally skipped.
- Child implementation worktree was removed:
- `/home/hare/Projects/yoi/.worktree/00001KW7835H0-old-pod-crate-cleanup`
- Child implementation branch was removed:
- `work/00001KW7835H0-old-pod-crate-cleanup`
- No root/original workspace cleanup was performed.
- Orchestration worktree is clean after worktree/branch cleanup.
Queue note:
- The Dashboard-queued chain related to Runtime Worker launch/fs-store/old Pod crate cleanup is now complete/closed.
---

32
Cargo.lock generated
View File

@ -2880,31 +2880,6 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "pod-registry"
version = "0.1.0"
dependencies = [
"fs4",
"libc",
"manifest",
"serde",
"serde_json",
"session-store",
"tempfile",
"thiserror 2.0.18",
]
[[package]]
name = "pod-store"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"session-store",
"tempfile",
"thiserror 2.0.18",
]
[[package]]
name = "pom"
version = "1.1.0"
@ -4688,11 +4663,10 @@ dependencies = [
"base64",
"client",
"crossterm 0.28.1",
"fs4",
"llm-engine",
"manifest",
"minijinja",
"pod-registry",
"pod-store",
"protocol",
"provider",
"pulldown-cmark",
@ -5910,8 +5884,6 @@ dependencies = [
"mcp",
"memory",
"minijinja",
"pod-registry",
"pod-store",
"protocol",
"provider",
"reqwest",
@ -5994,7 +5966,6 @@ dependencies = [
"client",
"manifest",
"memory",
"pod-store",
"project-record",
"serde",
"serde_json",
@ -6038,6 +6009,7 @@ dependencies = [
"axum",
"chrono",
"futures",
"manifest",
"project-record",
"protocol",
"reqwest",

View File

@ -12,10 +12,8 @@ members = [
"crates/worker-runtime",
"crates/plugin-pdk",
"crates/yoi",
"crates/pod-store",
"crates/protocol",
"crates/provider",
"crates/pod-registry",
"crates/session-metrics",
"crates/session-analytics",
"crates/lint-common",
@ -40,10 +38,8 @@ default-members = [
"crates/worker-runtime",
"crates/plugin-pdk",
"crates/yoi",
"crates/pod-store",
"crates/protocol",
"crates/provider",
"crates/pod-registry",
"crates/session-metrics",
"crates/session-analytics",
"crates/lint-common",
@ -75,8 +71,6 @@ worker = { path = "crates/worker" }
worker-runtime = { path = "crates/worker-runtime" }
yoi-plugin-pdk = { path = "crates/plugin-pdk" }
yoi = { path = "crates/yoi" }
pod-registry = { path = "crates/pod-registry" }
pod-store = { path = "crates/pod-store" }
protocol = { path = "crates/protocol" }
provider = { path = "crates/provider" }
session-metrics = { path = "crates/session-metrics" }

View File

@ -16,7 +16,7 @@ Owns:
Does not own:
- product command names (`yoi`)
- Worker state authority (`worker`, `pod-store`, `session-store`)
- Worker state authority (`worker`, `session-store` worker metadata)
- UI rendering (`tui`)
- Engine turn semantics (`llm-engine`)

View File

@ -19,7 +19,7 @@ Does not own:
- Worker names, sockets, process lifecycle, or scope delegation (`worker`)
- product CLI shape (`yoi`)
- provider catalog and secret resolution (`provider`, `secrets`)
- durable Worker current state (`pod-store`)
- durable Worker current state (`session-store` worker metadata)
## Design notes

View File

@ -88,9 +88,9 @@ pub fn sessions_dir() -> Option<PathBuf> {
sessions_dir_from_data_dir(data_dir())
}
/// `<runtime_dir>/workers.json` — machine-wide Worker allocation registry
pub fn pod_registry_path() -> Option<PathBuf> {
pod_registry_path_from_runtime_dir(runtime_dir())
/// `<runtime_dir>/workers.json` — machine-wide Worker allocation table
pub fn worker_allocation_path() -> Option<PathBuf> {
worker_allocation_path_from_runtime_dir(runtime_dir())
}
/// `<runtime_dir>/<worker_name>/` — Worker ごとのランタイムディレクトリ。
@ -104,8 +104,8 @@ pub fn worker_runtime_dir(worker_name: &str) -> Option<PathBuf> {
/// `RuntimeDir::socket_path()` で、Worker 名が分かっている外部 (TUI の
/// attach フロー等) からの**予測**はこの関数で行う。両者は同じパス
/// を返すことが期待される。
pub fn pod_socket_path(worker_name: &str) -> Option<PathBuf> {
pod_socket_path_from_runtime_dir(runtime_dir(), worker_name)
pub fn worker_socket_path(worker_name: &str) -> Option<PathBuf> {
worker_socket_path_from_runtime_dir(runtime_dir(), worker_name)
}
// ---- internals --------------------------------------------------------------
@ -183,7 +183,7 @@ fn sessions_dir_from_data_dir(data_dir: Option<PathBuf>) -> Option<PathBuf> {
Some(data_dir?.join("sessions"))
}
fn pod_registry_path_from_runtime_dir(runtime_dir: Option<PathBuf>) -> Option<PathBuf> {
fn worker_allocation_path_from_runtime_dir(runtime_dir: Option<PathBuf>) -> Option<PathBuf> {
Some(runtime_dir?.join("workers.json"))
}
@ -194,7 +194,7 @@ fn worker_runtime_dir_from_runtime_dir(
Some(runtime_dir?.join(worker_name))
}
fn pod_socket_path_from_runtime_dir(
fn worker_socket_path_from_runtime_dir(
runtime_dir: Option<PathBuf>,
worker_name: &str,
) -> Option<PathBuf> {
@ -396,7 +396,7 @@ mod tests {
PathBuf::from("/sand/sessions")
);
assert_eq!(
pod_registry_path_from_runtime_dir(runtime_dir.clone()).unwrap(),
worker_allocation_path_from_runtime_dir(runtime_dir.clone()).unwrap(),
PathBuf::from("/sand/run/workers.json")
);
assert_eq!(
@ -404,7 +404,7 @@ mod tests {
PathBuf::from("/sand/run/foo")
);
assert_eq!(
pod_socket_path_from_runtime_dir(runtime_dir, "foo").unwrap(),
worker_socket_path_from_runtime_dir(runtime_dir, "foo").unwrap(),
PathBuf::from("/sand/run/foo/sock")
);
}

View File

@ -266,7 +266,7 @@ impl Scope {
/// Allow rules with their targets resolved to absolute paths.
///
/// Used by the pod-registry, where every Worker's allocation
/// Used by the worker-allocation, where every Worker's allocation
/// must be expressed in absolute terms so prefix comparisons are
/// meaningful across processes.
pub fn allow_rules(&self) -> Vec<ScopeRule> {

View File

@ -1,17 +0,0 @@
[package]
name = "pod-registry"
version = "0.1.0"
edition.workspace = true
license.workspace = true
[dependencies]
fs4 = { workspace = true, features = ["sync"] }
libc = { workspace = true }
manifest = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
session-store = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View File

@ -1,30 +0,0 @@
# pod-registry
## Role
`pod-registry` is the legacy-named crate that tracks live Worker process ownership and delegated scope locks at runtime.
## Boundaries
Owns:
- machine-local live Worker registration
- collision detection for running Worker names
- delegated scope lock bookkeeping
- registry cleanup hooks for stopped or unreachable children
Does not own:
- durable Worker metadata (`pod-store`)
- replayable session logs (`session-store`)
- socket protocol definitions (`protocol`)
- project work item state
## Design notes
The registry is a runtime coordination mechanism. It can help decide whether a Worker is live or colliding, but durable visibility/restoration should be backed by Worker metadata when possible.
## See also
- [`../../docs/design/worker-session-state.md`](../../docs/design/worker-session-state.md)
- [`../../docs/design/tool-permissions-scope.md`](../../docs/design/tool-permissions-scope.md)

View File

@ -1,37 +0,0 @@
//! Machine-wide Worker allocation registry.
//!
//! A single JSON file at `<runtime_dir>/workers.json` records every live
//! Worker's allocation (see [`manifest::paths::pod_registry_path`] for
//! how the path is resolved). File-level `flock(2)` serialises access
//! across processes so spawn sequences from unrelated Workers can't race.
//!
//! Each Worker, when starting, acquires the lock, reclaims stale entries
//! (Workers whose PID has died), checks that its requested write scope
//! does not overlap any other allocation's effective write scope, and
//! registers itself. When it exits normally, it removes its entry and
//! returns delegated scope to its `delegated_from` parent. Crash
//! recovery rides on the next Worker that opens the file — no background
//! reaper.
mod conflict;
mod error;
mod lifecycle;
mod mutate;
mod table;
#[cfg(test)]
mod test_util;
pub use conflict::{
ConflictOwner, find_conflict_owner, find_conflict_owners, is_within_effective_write,
};
pub use error::ScopeLockError;
pub use lifecycle::{
ScopeAllocationGuard, SegmentLockInfo, adopt_allocation, install_top_level,
install_top_level_with_deny, lookup_segment, update_segment,
};
pub use mutate::{
delegate_scope, reclaim_delegated_scope, reclaim_stale, reclaim_stale_with, register_worker,
register_worker_with_deny, release_worker,
};
pub use table::{Allocation, LockFile, LockFileGuard, default_registry_path};

View File

@ -1,15 +0,0 @@
[package]
name = "pod-store"
description = "Legacy-named durable Worker metadata/state persistence"
version = "0.1.0"
edition.workspace = true
license.workspace = true
[dependencies]
session-store = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
tempfile = { workspace = true }

View File

@ -1,30 +0,0 @@
# pod-store
## Role
`pod-store` is the legacy-named crate that owns current Worker metadata keyed by Worker name.
## Boundaries
Owns:
- persisted Worker metadata files
- current active/pending session pointers
- resolved manifest snapshots for restoration
- parent-visible spawned-child metadata
- restoration labels and diagnostics derived from metadata
Does not own:
- replayable conversation logs (`session-store`)
- live process locks or socket reachability (`pod-registry`, `client`)
- product CLI behavior (`yoi`)
- model turn execution (`llm-engine`)
## Design notes
Worker metadata is intentionally thin. It should answer current-state questions without duplicating transcripts or becoming a second session log.
## See also
- [`../../docs/design/worker-session-state.md`](../../docs/design/worker-session-state.md)

View File

@ -17,7 +17,7 @@ Does not own:
- Unix socket implementation details (`client`, `worker`)
- TUI rendering (`tui`)
- Engine history semantics (`llm-engine`)
- durable storage (`session-store`, `pod-store`)
- durable storage (`session-store`, `session-store` worker metadata)
## Design notes

View File

@ -99,7 +99,7 @@ pub enum Method {
///
/// Delivered as `Method::WorkerEvent` over the parent's Unix socket. The
/// parent Controller always applies variant-specific side effects
/// (registry / pod-registry updates). Agent-visible variants are also
/// (registry / worker-allocation updates). Agent-visible variants are also
/// queued into the notification buffer; control-plane-only variants are
/// not injected into the parent's LLM context.
///

View File

@ -15,8 +15,8 @@ Owns:
Does not own:
- current Worker-name metadata (`pod-store`)
- live process/socket discovery (`pod-registry`, `client`)
- current Worker-name metadata (`session-store` worker metadata)
- live process/socket discovery (`worker-allocation`, `client`)
- UI state (`tui`)
- generated memory summaries (`memory`)

View File

@ -37,6 +37,7 @@ pub mod segment;
pub mod segment_log;
pub mod store;
pub mod system_item;
pub mod worker_metadata;
pub use event_trace::{TraceEntry, TracePayload};
pub use fs_store::FsStore;
@ -52,6 +53,11 @@ pub use segment::{
pub use segment_log::{LogEntry, RestoredState, SegmentOrigin, collect_state};
pub use store::{Store, StoreError};
pub use system_item::{SystemItem, SystemReminder, SystemReminderSource, render_worker_event};
pub use worker_metadata::{
CombinedStore, FsWorkerStore, WorkerActiveSegmentRef, WorkerMetadata, WorkerMetadataStore,
WorkerPeer, WorkerReclaimedChild, WorkerSpawnedChild, WorkerSpawnedScopeRule, WorkerStoreError,
validate_worker_name,
};
/// Session identifier — the fork-tree root. UUID v7 (time-ordered).
///

View File

@ -12,8 +12,8 @@
//! model remain session JSONL history. Socket and callback paths are last-known
//! runtime hints, not proof of liveness.
use crate::{SegmentId, SessionId};
use serde::{Deserialize, Serialize};
use session_store::{SegmentId, SessionId};
use std::fs;
use std::path::PathBuf;
@ -26,8 +26,8 @@ pub enum WorkerStoreError {
#[error("serialization error: {0}")]
Serde(#[from] serde_json::Error),
#[error("invalid pod name: {0}")]
InvalidPodName(String),
#[error("invalid worker name: {0}")]
InvalidWorkerName(String),
}
/// Active Session/Segment pointer for a Worker.
@ -284,13 +284,13 @@ impl FsWorkerStore {
Ok(Self { root })
}
fn pod_dir(&self, worker_name: &str) -> Result<PathBuf, WorkerStoreError> {
fn worker_dir(&self, worker_name: &str) -> Result<PathBuf, WorkerStoreError> {
validate_worker_name(worker_name)?;
Ok(self.root.join(worker_name))
}
fn metadata_path(&self, worker_name: &str) -> Result<PathBuf, WorkerStoreError> {
Ok(self.pod_dir(worker_name)?.join("metadata.json"))
Ok(self.worker_dir(worker_name)?.join("metadata.json"))
}
}
@ -364,7 +364,7 @@ pub fn validate_worker_name(worker_name: &str) -> Result<(), WorkerStoreError> {
|| worker_name.contains('/')
|| worker_name.contains('\0')
{
return Err(WorkerStoreError::InvalidPodName(worker_name.to_string()));
return Err(WorkerStoreError::InvalidWorkerName(worker_name.to_string()));
}
Ok(())
}
@ -372,61 +372,58 @@ pub fn validate_worker_name(worker_name: &str) -> Result<(), WorkerStoreError> {
/// Convenience composition for callers that want one handle carrying separate
/// session-log and Worker-state roots.
#[derive(Clone)]
pub struct CombinedStore<S, P> {
pub struct CombinedStore<S, W> {
pub session_store: S,
pub pod_store: P,
pub worker_metadata_store: W,
}
impl<S, P> CombinedStore<S, P> {
pub fn new(session_store: S, pod_store: P) -> Self {
impl<S, W> CombinedStore<S, W> {
pub fn new(session_store: S, worker_metadata_store: W) -> Self {
Self {
session_store,
pod_store,
worker_metadata_store,
}
}
}
impl<S, P> session_store::Store for CombinedStore<S, P>
impl<S, W> crate::Store for CombinedStore<S, W>
where
S: session_store::Store,
P: Send + Sync,
S: crate::Store,
W: Send + Sync,
{
fn append(
&self,
session_id: SessionId,
segment_id: SegmentId,
entry: &session_store::LogEntry,
) -> Result<(), session_store::StoreError> {
entry: &crate::LogEntry,
) -> Result<(), crate::StoreError> {
self.session_store.append(session_id, segment_id, entry)
}
fn read_all(
&self,
session_id: SessionId,
segment_id: SegmentId,
) -> Result<Vec<session_store::LogEntry>, session_store::StoreError> {
) -> Result<Vec<crate::LogEntry>, crate::StoreError> {
self.session_store.read_all(session_id, segment_id)
}
fn list_sessions(&self) -> Result<Vec<SessionId>, session_store::StoreError> {
fn list_sessions(&self) -> Result<Vec<SessionId>, crate::StoreError> {
self.session_store.list_sessions()
}
fn list_segments(
&self,
session_id: SessionId,
) -> Result<Vec<SegmentId>, session_store::StoreError> {
fn list_segments(&self, session_id: SessionId) -> Result<Vec<SegmentId>, crate::StoreError> {
self.session_store.list_segments(session_id)
}
fn lookup_session_of(
&self,
segment_id: SegmentId,
) -> Result<Option<SessionId>, session_store::StoreError> {
) -> Result<Option<SessionId>, crate::StoreError> {
self.session_store.lookup_session_of(segment_id)
}
fn create_segment(
&self,
session_id: SessionId,
segment_id: SegmentId,
entries: &[session_store::LogEntry],
) -> Result<(), session_store::StoreError> {
entries: &[crate::LogEntry],
) -> Result<(), crate::StoreError> {
self.session_store
.create_segment(session_id, segment_id, entries)
}
@ -434,7 +431,7 @@ where
&self,
session_id: SessionId,
segment_id: SegmentId,
) -> Result<bool, session_store::StoreError> {
) -> Result<bool, crate::StoreError> {
self.session_store.exists(session_id, segment_id)
}
fn truncate(
@ -442,7 +439,7 @@ where
session_id: SessionId,
segment_id: SegmentId,
entries_len: usize,
) -> Result<(), session_store::StoreError> {
) -> Result<(), crate::StoreError> {
self.session_store
.truncate(session_id, segment_id, entries_len)
}
@ -450,39 +447,39 @@ where
&self,
session_id: SessionId,
segment_id: SegmentId,
) -> Result<usize, session_store::StoreError> {
) -> Result<usize, crate::StoreError> {
self.session_store.read_entry_count(session_id, segment_id)
}
fn append_trace(
&self,
session_id: SessionId,
segment_id: SegmentId,
entry: &session_store::TraceEntry,
) -> Result<(), session_store::StoreError> {
entry: &crate::TraceEntry,
) -> Result<(), crate::StoreError> {
self.session_store
.append_trace(session_id, segment_id, entry)
}
}
impl<S, P> WorkerMetadataStore for CombinedStore<S, P>
impl<S, W> WorkerMetadataStore for CombinedStore<S, W>
where
S: Send + Sync,
P: WorkerMetadataStore,
W: WorkerMetadataStore,
{
fn write(&self, metadata: &WorkerMetadata) -> Result<(), WorkerStoreError> {
self.pod_store.write(metadata)
self.worker_metadata_store.write(metadata)
}
fn read_by_name(&self, worker_name: &str) -> Result<Option<WorkerMetadata>, WorkerStoreError> {
self.pod_store.read_by_name(worker_name)
self.worker_metadata_store.read_by_name(worker_name)
}
fn list_names(&self) -> Result<Vec<String>, WorkerStoreError> {
self.pod_store.list_names()
self.worker_metadata_store.list_names()
}
fn root_dir(&self) -> Option<PathBuf> {
self.pod_store.root_dir()
self.worker_metadata_store.root_dir()
}
fn delete_by_name(&self, worker_name: &str) -> Result<(), WorkerStoreError> {
self.pod_store.delete_by_name(worker_name)
self.worker_metadata_store.delete_by_name(worker_name)
}
}
@ -491,15 +488,15 @@ mod tests {
use super::*;
#[test]
fn pod_metadata_manifest_snapshot_roundtrips() {
fn worker_metadata_manifest_snapshot_roundtrips() {
let mut metadata = WorkerMetadata::new(
"profile-pod",
"profile-worker",
Some(WorkerActiveSegmentRef::pending_segment(
session_store::new_session_id(),
crate::new_session_id(),
)),
);
metadata.resolved_manifest_snapshot = Some(serde_json::json!({
"pod": { "name": "profile-pod" },
"worker": { "name": "profile-worker" },
"profile": { "source": { "kind": "path", "path": "/profiles/coder.lua" } }
}));
@ -510,22 +507,22 @@ mod tests {
}
#[test]
fn fs_store_writes_under_pod_state_root_only() {
fn fs_store_writes_under_worker_state_root_only() {
let tmp = tempfile::TempDir::new().unwrap();
let session_root = tmp.path().join("sessions");
let pod_root = tmp.path().join("workers");
let worker_root = tmp.path().join("workers");
fs::create_dir_all(&session_root).unwrap();
let store = FsWorkerStore::new(&pod_root).unwrap();
let store = FsWorkerStore::new(&worker_root).unwrap();
store
.write(&WorkerMetadata::new(
"agent",
Some(WorkerActiveSegmentRef::pending_segment(
session_store::new_session_id(),
crate::new_session_id(),
)),
))
.unwrap();
assert!(pod_root.join("agent/metadata.json").exists());
assert!(worker_root.join("agent/metadata.json").exists());
assert!(!session_root.join("workers/agent/metadata.json").exists());
}
@ -540,16 +537,16 @@ mod tests {
scope_delegated: vec![],
callback_address: std::path::Path::new("/tmp/parent.sock").into(),
});
metadata.resolved_manifest_snapshot = Some(serde_json::json!({"pod":{"name":"agent"}}));
metadata.resolved_manifest_snapshot = Some(serde_json::json!({"worker":{"name":"agent"}}));
store.write(&metadata).unwrap();
let snapshot = serde_json::json!({"pod":{"name":"updated"}});
let snapshot = serde_json::json!({"worker":{"name":"updated"}});
store
.set_active(
"agent",
Some(WorkerActiveSegmentRef::active_segment(
session_store::new_session_id(),
session_store::new_segment_id(),
crate::new_session_id(),
crate::new_segment_id(),
)),
Some(snapshot.clone()),
)
@ -564,10 +561,10 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let store = FsWorkerStore::new(tmp.path()).unwrap();
let active = WorkerActiveSegmentRef::active_segment(
session_store::new_session_id(),
session_store::new_segment_id(),
crate::new_session_id(),
crate::new_segment_id(),
);
let snapshot = serde_json::json!({"pod":{"name":"agent"}});
let snapshot = serde_json::json!({"worker":{"name":"agent"}});
store
.set_active("agent", Some(active.clone()), Some(snapshot.clone()))
.unwrap();
@ -592,10 +589,10 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let store = FsWorkerStore::new(tmp.path()).unwrap();
let active = WorkerActiveSegmentRef::active_segment(
session_store::new_session_id(),
session_store::new_segment_id(),
crate::new_session_id(),
crate::new_segment_id(),
);
let snapshot = serde_json::json!({"pod":{"name":"agent"}});
let snapshot = serde_json::json!({"worker":{"name":"agent"}});
store
.set_active("agent", Some(active.clone()), Some(snapshot.clone()))
.unwrap();

View File

@ -22,8 +22,7 @@ toml = { workspace = true }
manifest = { workspace = true }
secrets = { workspace = true }
session-store = { workspace = true }
pod-store = { workspace = true }
pod-registry = { workspace = true }
fs4 = { workspace = true }
provider = { workspace = true }
ticket = { workspace = true }
serde = { workspace = true, features = ["derive"] }

View File

@ -16,7 +16,7 @@ Owns:
Does not own:
- durable transcript authority (`session-store`)
- Worker current state (`pod-store`)
- Worker current state (`session-store` worker metadata)
- Worker lifecycle policy (`worker`)
- product CLI ownership (`yoi`)

View File

@ -132,7 +132,7 @@ fn resolve_socket(worker_name: &str, override_path: Option<PathBuf>) -> PathBuf
if let Some(p) = override_path {
return p;
}
manifest::paths::pod_socket_path(worker_name).unwrap_or_else(|| {
manifest::paths::worker_socket_path(worker_name).unwrap_or_else(|| {
PathBuf::from("/tmp")
.join("yoi")
.join(worker_name)
@ -317,7 +317,7 @@ async fn connect_live_pod(
if !allow_registry_fallback {
return None;
}
let registry_socket = picker::live_socket_for_pod(worker_name)?;
let registry_socket = picker::live_socket_for_worker(worker_name)?;
if registry_socket == preferred_socket {
return None;
}

View File

@ -19,7 +19,6 @@ use crossterm::event::{
Event as TermEvent, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
poll, read,
};
use pod_store::FsWorkerStore;
use protocol::stream::{JsonLineReader, JsonLineWriter};
use protocol::{ErrorCode, Event, Method, Segment, WorkerStatus};
use ratatui::Frame;
@ -31,6 +30,7 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget, Wrap};
use serde::Serialize;
use session_store::FsStore;
use session_store::FsWorkerStore;
use ticket::config::{GitBranchName, TicketConfig, TicketOrchestrationConfig};
use ticket::{
LocalTicketBackend, MarkdownText, TicketBackend, TicketIdOrSlug, TicketStateChange,
@ -46,7 +46,7 @@ use crate::role_session_registry::{
};
use crate::worker_list::{
StoredMetadataState, WorkerList, WorkerListEntry, WorkerVisibilitySource,
read_reachable_live_pod_infos, read_stored_worker_infos,
read_reachable_live_worker_infos, read_stored_worker_infos,
};
#[cfg(not(feature = "e2e-test"))]
use crate::workspace_panel::build_workspace_panel;
@ -522,9 +522,9 @@ fn default_store_dir() -> Result<PathBuf, DashboardError> {
})
}
fn default_pod_store_dir() -> Result<PathBuf, DashboardError> {
fn default_worker_metadata_dir() -> Result<PathBuf, DashboardError> {
manifest::paths::data_dir()
.map(|dir| dir.join("pods"))
.map(|dir| dir.join("workers"))
.ok_or_else(|| {
DashboardError::Io(io::Error::new(
io::ErrorKind::NotFound,
@ -3423,9 +3423,10 @@ async fn load_worker_list(
) -> Result<WorkerList, DashboardError> {
let store_dir = default_store_dir()?;
let store = FsStore::new(&store_dir)?;
let pod_store = FsWorkerStore::new(default_pod_store_dir()?).map_err(io::Error::other)?;
let stored = read_stored_worker_infos(&store, &pod_store)?;
let live = read_reachable_live_pod_infos(&store)
let worker_metadata_store =
FsWorkerStore::new(default_worker_metadata_dir()?).map_err(io::Error::other)?;
let stored = read_stored_worker_infos(&store, &worker_metadata_store)?;
let live = read_reachable_live_worker_infos(&store)
.await
.unwrap_or_default();
Ok(WorkerList::from_workspace_sources(

View File

@ -1,7 +1,7 @@
//! Inline-viewport "pick a Worker to attach or restore" UX.
//!
//! Reads live Worker allocations from the runtime registry and stopped Worker state
//! from the pod-store name-keyed metadata. Picking a live row attaches to
//! from the session-store worker metadata name-keyed metadata. Picking a live row attaches to
//! its socket; picking a stopped row restores via the Worker runtime command.
use std::io;
@ -9,7 +9,6 @@ use std::path::PathBuf;
use std::time::Duration;
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
use pod_store::FsWorkerStore;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Layout};
@ -18,11 +17,12 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use ratatui::{Frame, TerminalOptions, Viewport};
use session_store::FsStore;
use session_store::FsWorkerStore;
use crate::worker_list::{
LiveWorkerInfo, StoredMetadataState, StoredWorkerInfo, WorkerList, WorkerListEntry,
WorkerVisibilitySource, live_socket_for_pod as worker_list_live_socket_for_pod,
read_reachable_live_pod_infos, read_stored_worker_infos,
WorkerVisibilitySource, live_socket_for_worker as worker_list_live_socket_for_worker,
read_reachable_live_worker_infos, read_stored_worker_infos,
};
const MAX_ROWS: usize = 10;
@ -156,9 +156,10 @@ fn list_for_options(
pub async fn run(options: PickerOptions) -> Result<PickerOutcome, PickerError> {
let store_dir = default_store_dir()?;
let store = FsStore::new(&store_dir)?;
let pod_store = FsWorkerStore::new(default_pod_store_dir()?).map_err(io::Error::other)?;
let stored_workers = read_stored_worker_infos(&store, &pod_store)?;
let live_workers = read_reachable_live_pod_infos(&store)
let worker_metadata_store =
FsWorkerStore::new(default_worker_metadata_dir()?).map_err(io::Error::other)?;
let stored_workers = read_stored_worker_infos(&store, &worker_metadata_store)?;
let live_workers = read_reachable_live_worker_infos(&store)
.await
.unwrap_or_default();
let mut list = list_for_options(&options, stored_workers, live_workers);
@ -223,9 +224,9 @@ fn default_store_dir() -> Result<PathBuf, PickerError> {
})
}
fn default_pod_store_dir() -> Result<PathBuf, PickerError> {
fn default_worker_metadata_dir() -> Result<PathBuf, PickerError> {
manifest::paths::data_dir()
.map(|dir| dir.join("pods"))
.map(|dir| dir.join("workers"))
.ok_or_else(|| {
PickerError::Io(io::Error::new(
io::ErrorKind::NotFound,
@ -235,8 +236,8 @@ fn default_pod_store_dir() -> Result<PathBuf, PickerError> {
})
}
pub(crate) fn live_socket_for_pod(worker_name: &str) -> Option<PathBuf> {
worker_list_live_socket_for_pod(worker_name)
pub(crate) fn live_socket_for_worker(worker_name: &str) -> Option<PathBuf> {
worker_list_live_socket_for_worker(worker_name)
}
fn make_inline_terminal() -> io::Result<Terminal<CrosstermBackend<io::Stdout>>> {

View File

@ -1,13 +1,15 @@
use std::collections::{BTreeMap, BTreeSet};
use std::io;
use std::fs::File;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::time::Duration;
use client::WorkerClient;
use pod_registry::{LockFileGuard, default_registry_path};
use pod_store::{WorkerActiveSegmentRef, WorkerMetadata, WorkerMetadataStore};
use manifest::paths;
use protocol::{Event, WorkerStatus};
use serde::Deserialize;
use session_store::{FsStore, SegmentId, SessionId};
use session_store::{WorkerActiveSegmentRef, WorkerMetadata, WorkerMetadataStore};
#[derive(Debug, Clone)]
pub(crate) struct WorkerList {
@ -314,11 +316,14 @@ pub(crate) enum WorkerEntryDiagnosticKind {
pub(crate) fn read_stored_worker_infos(
store: &FsStore,
pod_store: &impl WorkerMetadataStore,
worker_metadata_store: &impl WorkerMetadataStore,
) -> Result<Vec<StoredWorkerInfo>, io::Error> {
let mut records = Vec::new();
for worker_name in pod_store.list_names().map_err(io::Error::other)? {
let info = match pod_store.read_by_name(&worker_name) {
for worker_name in worker_metadata_store
.list_names()
.map_err(io::Error::other)?
{
let info = match worker_metadata_store.read_by_name(&worker_name) {
Ok(Some(metadata)) => stored_info_from_metadata(store, worker_name, metadata),
Ok(None) => corrupt_stored_info(
worker_name,
@ -331,16 +336,24 @@ pub(crate) fn read_stored_worker_infos(
Ok(records)
}
pub(crate) fn read_live_pod_infos() -> Result<Vec<LiveWorkerInfo>, io::Error> {
let path = default_registry_path()?;
let guard = LockFileGuard::open(&path)?;
Ok(guard
.data()
pub(crate) fn read_live_worker_infos() -> Result<Vec<LiveWorkerInfo>, io::Error> {
let path = paths::worker_allocation_path().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"could not resolve worker allocation path",
)
})?;
let table = match read_worker_allocation_table(&path) {
Ok(table) => table,
Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(err),
};
Ok(table
.allocations
.iter()
.into_iter()
.map(|allocation| LiveWorkerInfo {
worker_name: allocation.worker_name.clone(),
socket_path: allocation.socket.clone(),
worker_name: allocation.worker_name,
socket_path: allocation.socket,
status: None,
reachable: false,
segment_id: allocation.segment_id,
@ -349,20 +362,49 @@ pub(crate) fn read_live_pod_infos() -> Result<Vec<LiveWorkerInfo>, io::Error> {
.collect())
}
pub(crate) async fn read_reachable_live_pod_infos(
store: &FsStore,
) -> Result<Vec<LiveWorkerInfo>, io::Error> {
let records = read_live_pod_infos()?;
probe_reachable_live_pod_infos(store, records).await
fn read_worker_allocation_table(path: &Path) -> Result<WorkerAllocationTable, io::Error> {
let mut file = File::open(path)?;
fs4::fs_std::FileExt::lock_shared(&file)?;
let mut contents = String::new();
let read_result = file.read_to_string(&mut contents);
let unlock_result = fs4::fs_std::FileExt::unlock(&file);
read_result?;
unlock_result?;
if contents.trim().is_empty() {
return Ok(WorkerAllocationTable::default());
}
serde_json::from_str(&contents).map_err(io::Error::other)
}
async fn probe_reachable_live_pod_infos(
#[derive(Debug, Default, Deserialize)]
struct WorkerAllocationTable {
#[serde(default)]
allocations: Vec<WorkerAllocationRecord>,
}
#[derive(Debug, Deserialize)]
struct WorkerAllocationRecord {
worker_name: String,
socket: PathBuf,
#[serde(default)]
segment_id: Option<SegmentId>,
}
pub(crate) async fn read_reachable_live_worker_infos(
store: &FsStore,
) -> Result<Vec<LiveWorkerInfo>, io::Error> {
let records = read_live_worker_infos()?;
probe_reachable_live_worker_infos(store, records).await
}
async fn probe_reachable_live_worker_infos(
_store: &FsStore,
records: Vec<LiveWorkerInfo>,
) -> Result<Vec<LiveWorkerInfo>, io::Error> {
let mut handles = Vec::with_capacity(records.len());
for record in records {
handles.push(tokio::spawn(probe_live_pod_info(record)));
handles.push(tokio::spawn(probe_live_worker_info(record)));
}
let mut reachable = Vec::with_capacity(handles.len());
@ -378,15 +420,15 @@ async fn probe_reachable_live_pod_infos(
Ok(reachable)
}
async fn probe_live_pod_info(mut record: LiveWorkerInfo) -> Result<LiveWorkerInfo, io::Error> {
async fn probe_live_worker_info(mut record: LiveWorkerInfo) -> Result<LiveWorkerInfo, io::Error> {
let status = probe_live_status(&record.socket_path).await?;
record.reachable = true;
record.status = status;
Ok(record)
}
pub(crate) fn live_socket_for_pod(worker_name: &str) -> Option<PathBuf> {
read_live_pod_infos()
pub(crate) fn live_socket_for_worker(worker_name: &str) -> Option<PathBuf> {
read_live_worker_infos()
.ok()?
.into_iter()
.find(|worker| worker.worker_name == worker_name)
@ -560,10 +602,10 @@ mod tests {
use std::sync::Arc;
use llm_engine::llm_client::types::RequestConfig;
use pod_store::FsWorkerStore;
use pod_store::{WorkerActiveSegmentRef, WorkerMetadataStore};
use protocol::stream::JsonLineWriter;
use session_store::FsWorkerStore;
use session_store::{LogEntry, Store, new_segment_id, new_session_id};
use session_store::{WorkerActiveSegmentRef, WorkerMetadataStore};
use tempfile::tempdir;
use tokio::net::UnixListener;
use tokio::sync::Barrier;
@ -877,7 +919,7 @@ mod tests {
let records = tokio::time::timeout(
LIVE_STATUS_PROBE_TIMEOUT * 3,
probe_reachable_live_pod_infos(&store, records),
probe_reachable_live_worker_infos(&store, records),
)
.await
.expect("status probes should complete")
@ -907,7 +949,7 @@ mod tests {
std::future::pending::<()>().await;
});
let records = probe_reachable_live_pod_infos(
let records = probe_reachable_live_worker_infos(
&store,
vec![live_probe_record("silent", socket_path.clone())],
)
@ -985,12 +1027,12 @@ mod tests {
fn read_stored_worker_infos_reports_corrupt_metadata() {
let dir = tempdir().unwrap();
let store = FsStore::new(dir.path()).unwrap();
let pod_store = FsWorkerStore::new(dir.path().join("pods")).unwrap();
let pod_dir = dir.path().join("pods").join("broken");
std::fs::create_dir_all(&pod_dir).unwrap();
std::fs::write(pod_dir.join("metadata.json"), "{not-json").unwrap();
let worker_metadata_store = FsWorkerStore::new(dir.path().join("workers")).unwrap();
let worker_metadata_dir = dir.path().join("workers").join("broken");
std::fs::create_dir_all(&worker_metadata_dir).unwrap();
std::fs::write(worker_metadata_dir.join("metadata.json"), "{not-json").unwrap();
let records = read_stored_worker_infos(&store, &pod_store).unwrap();
let records = read_stored_worker_infos(&store, &worker_metadata_store).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].worker_name, "broken");
assert!(matches!(
@ -1003,10 +1045,10 @@ mod tests {
fn read_stored_worker_infos_reads_metadata() {
let dir = tempdir().unwrap();
let store = FsStore::new(dir.path()).unwrap();
let pod_store = FsWorkerStore::new(dir.path().join("pods")).unwrap();
let worker_metadata_store = FsWorkerStore::new(dir.path().join("workers")).unwrap();
let session_id = new_session_id();
let segment_id = new_segment_id();
pod_store
worker_metadata_store
.write(&WorkerMetadata::new(
"agent",
Some(WorkerActiveSegmentRef::active_segment(
@ -1015,7 +1057,7 @@ mod tests {
))
.unwrap();
let records = read_stored_worker_infos(&store, &pod_store).unwrap();
let records = read_stored_worker_infos(&store, &worker_metadata_store).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].worker_name, "agent");
assert_eq!(records[0].metadata_state, StoredMetadataState::Present);

View File

@ -1,32 +1,9 @@
use crate::execution::WorkerExecutionStatus;
use crate::identity::{RuntimeId, WorkerId, WorkerRef};
use crate::interaction::WorkerInput;
use serde::{Deserialize, Serialize};
/// Intent supplied when a Worker is created.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum WorkerIntent {
Assistant {
#[serde(default, skip_serializing_if = "Option::is_none")]
purpose: Option<String>,
},
Task {
objective: String,
},
Role {
role: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
purpose: Option<String>,
},
}
impl Default for WorkerIntent {
fn default() -> Self {
Self::Assistant { purpose: None }
}
}
/// Profile selector boundary. This is a selector, not a resolved config bundle.
/// Profile selector boundary. This is a selector, not a resolved config bundle.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum ProfileSelector {
@ -48,77 +25,21 @@ pub struct ConfigBundleRef {
pub digest: String,
}
/// Requested capability name plus optional human-readable reason.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityRequest {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
impl CapabilityRequest {
pub fn named(name: impl Into<String>) -> Self {
Self {
name: name.into(),
reason: None,
}
}
}
/// Opaque workspace reference supplied by a caller.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkspaceRef {
pub name: String,
pub reference: String,
}
/// Opaque mount reference supplied by a caller.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MountRef {
pub name: String,
pub reference: String,
}
/// Worker creation request for the catalog/lifecycle API.
/// Canonical Runtime Worker creation request.
///
/// Browser/product launch semantics are resolved by a backend before this
/// request is built. The request contains only durable Runtime identity inputs:
/// a backend-decided profile selector, a previously synced ConfigBundle identity,
/// and optional initial user input that is committed in the same transaction as
/// Worker catalog/transcript persistence. It carries no cwd/workspace path, tool
/// scope, credential, socket/session path, raw config body, or execution binding
/// internals.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateWorkerRequest {
pub intent: WorkerIntent,
pub profile: ProfileSelector,
pub config_bundle: ConfigBundleRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_bundle: Option<ConfigBundleRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requested_capabilities: Vec<CapabilityRequest>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workspace_refs: Vec<WorkspaceRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mount_refs: Vec<MountRef>,
}
impl Default for CreateWorkerRequest {
fn default() -> Self {
Self {
intent: WorkerIntent::default(),
profile: ProfileSelector::default(),
config_bundle: None,
requested_capabilities: Vec::new(),
workspace_refs: Vec::new(),
mount_refs: Vec::new(),
}
}
}
impl CreateWorkerRequest {
/// Create a tools-less Worker using runtime-local default resources.
pub fn tools_less(intent: WorkerIntent, profile: ProfileSelector) -> Self {
Self {
intent,
profile,
config_bundle: None,
requested_capabilities: Vec::new(),
workspace_refs: Vec::new(),
mount_refs: Vec::new(),
}
}
pub initial_input: Option<WorkerInput>,
}
/// Worker lifecycle status for the in-memory embedded runtime.
@ -144,10 +65,8 @@ pub struct WorkerSummary {
pub worker_id: WorkerId,
pub status: WorkerStatus,
pub execution: WorkerExecutionStatus,
pub intent: WorkerIntent,
pub profile: ProfileSelector,
pub requested_capability_count: usize,
pub has_config_bundle: bool,
pub config_bundle: ConfigBundleRef,
pub transcript_len: usize,
pub last_event_id: u64,
}
@ -160,16 +79,8 @@ pub struct WorkerDetail {
pub worker_id: WorkerId,
pub status: WorkerStatus,
pub execution: WorkerExecutionStatus,
pub intent: WorkerIntent,
pub profile: ProfileSelector,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_bundle: Option<ConfigBundleRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requested_capabilities: Vec<CapabilityRequest>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub workspace_refs: Vec<WorkspaceRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub mount_refs: Vec<MountRef>,
pub config_bundle: ConfigBundleRef,
pub transcript_len: usize,
pub last_event_id: u64,
}

View File

@ -23,6 +23,9 @@ pub enum RuntimeError {
actual_runtime_id: RuntimeId,
},
#[error("initial worker input must be user input, got {kind}")]
InvalidInitialInputKind { kind: String },
#[error("worker {worker_id} was not found in runtime {runtime_id}")]
WorkerNotFound {
runtime_id: RuntimeId,
@ -35,6 +38,9 @@ pub enum RuntimeError {
message: String,
},
#[error("worker creation has no execution backend: {message}")]
ExecutionBackendUnavailable { message: String },
#[error("worker {worker_id} execution {operation:?} returned {outcome:?}: {message}")]
WorkerExecutionRejected {
worker_id: WorkerId,

View File

@ -18,9 +18,29 @@ use std::sync::Arc;
pub enum WorkerExecutionBackendKind {
#[default]
Unconnected,
/// A durable execution binding was restored, but no live handle was recovered.
Stale,
Connected,
}
/// Durable, non-authority execution binding projection.
///
/// This records only enough identity to diagnose stale mappings after restore.
/// It is not a live handle and must not contain sockets, paths, credentials, or
/// provider-private authority.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerExecutionBindingIdentity {
pub backend_id: String,
}
impl WorkerExecutionBindingIdentity {
pub fn from_handle(handle: &WorkerExecutionHandle) -> Self {
Self {
backend_id: handle.backend_id.clone(),
}
}
}
/// Current execution-side run state for a Worker.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
@ -131,6 +151,8 @@ pub struct WorkerExecutionStatus {
pub backend: WorkerExecutionBackendKind,
pub run_state: WorkerExecutionRunState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub binding: Option<WorkerExecutionBindingIdentity>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_result: Option<WorkerExecutionResult>,
}
@ -143,10 +165,22 @@ impl WorkerExecutionStatus {
Self {
backend: WorkerExecutionBackendKind::Connected,
run_state,
binding: None,
last_result: None,
}
}
pub fn stale(mut previous: Self) -> Self {
previous.backend = WorkerExecutionBackendKind::Stale;
previous.run_state = WorkerExecutionRunState::Unconnected;
previous
}
pub fn with_binding(mut self, binding: WorkerExecutionBindingIdentity) -> Self {
self.binding = Some(binding);
self
}
pub fn with_result(mut self, result: WorkerExecutionResult) -> Self {
self.run_state = result.run_state;
self.last_result = Some(result);

View File

@ -2,6 +2,7 @@ use crate::catalog::{CreateWorkerRequest, WorkerStatus};
use crate::config_bundle::ConfigBundle;
use crate::diagnostics::RuntimeDiagnostic;
use crate::error::RuntimeError;
use crate::execution::WorkerExecutionStatus;
use crate::identity::{RuntimeId, WorkerId, WorkerRef};
use crate::management::{RuntimeBackendKind, RuntimeLimits, RuntimeStatus};
use crate::observation::{
@ -376,6 +377,7 @@ pub(crate) struct PersistedWorkerRecord {
pub(crate) worker_id: WorkerId,
pub(crate) status: WorkerStatus,
pub(crate) request: CreateWorkerRequest,
pub(crate) execution: WorkerExecutionStatus,
pub(crate) transcript: Vec<TranscriptEntry>,
pub(crate) next_transcript_sequence: u64,
pub(crate) last_event_id: u64,
@ -473,6 +475,8 @@ struct WorkerSnapshot {
worker_id: WorkerId,
status: WorkerStatus,
request: CreateWorkerRequest,
#[serde(default = "WorkerExecutionStatus::unconnected")]
execution: WorkerExecutionStatus,
next_transcript_sequence: u64,
last_event_id: u64,
}
@ -485,6 +489,7 @@ impl WorkerSnapshot {
worker_id: worker.worker_id.clone(),
status: worker.status,
request: worker.request.clone(),
execution: worker.execution.clone(),
next_transcript_sequence: worker.next_transcript_sequence,
last_event_id: worker.last_event_id,
}
@ -530,6 +535,7 @@ impl WorkerSnapshot {
worker_id: self.worker_id,
status: self.status,
request: self.request,
execution: self.execution,
transcript,
next_transcript_sequence: self.next_transcript_sequence,
last_event_id: self.last_event_id,

View File

@ -824,9 +824,11 @@ fn status_for_runtime_error(error: &RuntimeError) -> StatusCode {
}
RuntimeError::RuntimeStopped { .. }
| RuntimeError::WorkerExecutionUnavailable { .. }
| RuntimeError::ExecutionBackendUnavailable { .. }
| RuntimeError::WorkerExecutionRejected { .. } => StatusCode::CONFLICT,
RuntimeError::LimitTooLarge { .. }
| RuntimeError::InvalidRequest(_)
| RuntimeError::InvalidInitialInputKind { .. }
| RuntimeError::ConfigBundleDigestMismatch { .. }
| RuntimeError::InvalidProfileSelector { .. }
| RuntimeError::UnsupportedConfigDeclaration { .. }
@ -846,9 +848,11 @@ fn code_for_runtime_error(error: &RuntimeError) -> &'static str {
RuntimeError::WrongRuntimeCursor { .. } => "wrong_runtime_cursor",
RuntimeError::WorkerNotFound { .. } => "worker_not_found",
RuntimeError::WorkerExecutionUnavailable { .. } => "worker_execution_unavailable",
RuntimeError::ExecutionBackendUnavailable { .. } => "execution_backend_unavailable",
RuntimeError::WorkerExecutionRejected { .. } => "worker_execution_rejected",
RuntimeError::LimitTooLarge { .. } => "limit_too_large",
RuntimeError::InvalidRequest(_) => "invalid_request",
RuntimeError::InvalidInitialInputKind { .. } => "invalid_initial_input_kind",
RuntimeError::ConfigBundleMissing { .. } => "config_bundle_missing",
RuntimeError::ConfigBundleDigestMismatch { .. } => "config_bundle_digest_mismatch",
RuntimeError::InvalidProfileSelector { .. } => "invalid_profile_selector",
@ -872,7 +876,10 @@ pub enum RuntimeHttpServerError {
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog::{CapabilityRequest, ProfileSelector, WorkerIntent};
use crate::catalog::{ConfigBundleRef, ProfileSelector};
use crate::config_bundle::{
ConfigBundle, ConfigBundleMetadata, ConfigBundleProvenance, ConfigProfileDescriptor,
};
use crate::execution::{
WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation,
WorkerExecutionResult, WorkerExecutionRunState, WorkerExecutionSpawnRequest,
@ -883,16 +890,38 @@ mod tests {
use axum::http::Method;
use tower::ServiceExt;
fn task_request(objective: &str) -> CreateWorkerRequest {
CreateWorkerRequest {
intent: WorkerIntent::Task {
objective: objective.to_string(),
fn test_bundle(profile: ProfileSelector) -> ConfigBundle {
ConfigBundle {
metadata: ConfigBundleMetadata {
id: "http-test-bundle".to_string(),
digest: String::new(),
revision: "test".to_string(),
workspace_id: "test-workspace".to_string(),
created_at: "test".to_string(),
provenance: ConfigBundleProvenance {
source: "test".to_string(),
detail: None,
},
},
profile: ProfileSelector::Builtin("builtin:coder".to_string()),
config_bundle: None,
requested_capabilities: vec![CapabilityRequest::named("read")],
workspace_refs: Vec::new(),
mount_refs: Vec::new(),
profiles: vec![ConfigProfileDescriptor {
selector: profile,
label: Some("test".to_string()),
}],
declarations: Vec::new(),
}
.with_computed_digest()
}
fn task_request(_objective: &str) -> CreateWorkerRequest {
let profile = ProfileSelector::Builtin("builtin:coder".to_string());
let bundle = test_bundle(profile.clone());
CreateWorkerRequest {
profile,
config_bundle: ConfigBundleRef {
id: bundle.metadata.id,
digest: bundle.metadata.digest,
},
initial_input: None,
}
}
@ -969,6 +998,11 @@ mod tests {
let runtime =
Runtime::with_execution_backend(RuntimeOptions::default(), Arc::new(AcceptingBackend))
.unwrap();
runtime
.store_config_bundle(test_bundle(ProfileSelector::Builtin(
"builtin:coder".to_string(),
)))
.unwrap();
let app = runtime_http_router(runtime.clone(), None);
let response = json_request(
@ -1091,15 +1125,89 @@ mod tests {
#[cfg(all(test, feature = "ws-server"))]
mod ws_tests {
use super::*;
use crate::catalog::{ConfigBundleRef, ProfileSelector};
use crate::config_bundle::{
ConfigBundle, ConfigBundleMetadata, ConfigBundleProvenance, ConfigProfileDescriptor,
};
use crate::execution::{
WorkerExecutionBackend, WorkerExecutionHandle, WorkerExecutionOperation,
WorkerExecutionResult, WorkerExecutionRunState, WorkerExecutionSpawnRequest,
WorkerExecutionSpawnResult,
};
use crate::interaction::WorkerInput;
use futures::{SinkExt, StreamExt};
use std::sync::Arc;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
struct WsBackend;
impl WorkerExecutionBackend for WsBackend {
fn backend_id(&self) -> &str {
"ws-test"
}
fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult {
WorkerExecutionSpawnResult::Connected {
handle: WorkerExecutionHandle::new(request.worker_ref, self.backend_id()),
run_state: WorkerExecutionRunState::Idle,
}
}
fn dispatch_input(
&self,
_handle: &WorkerExecutionHandle,
_input: WorkerInput,
) -> WorkerExecutionResult {
WorkerExecutionResult::accepted(
WorkerExecutionOperation::Input,
WorkerExecutionRunState::Idle,
)
}
}
fn ws_test_bundle(profile: ProfileSelector) -> ConfigBundle {
ConfigBundle {
metadata: ConfigBundleMetadata {
id: "ws-test-bundle".to_string(),
digest: String::new(),
revision: "test".to_string(),
workspace_id: "test".to_string(),
created_at: "test".to_string(),
provenance: ConfigBundleProvenance {
source: "test".to_string(),
detail: None,
},
},
profiles: vec![ConfigProfileDescriptor {
selector: profile,
label: Some("ws".to_string()),
}],
declarations: Vec::new(),
}
.with_computed_digest()
}
fn ws_create_request() -> CreateWorkerRequest {
let bundle = ws_test_bundle(ProfileSelector::RuntimeDefault);
CreateWorkerRequest {
profile: ProfileSelector::RuntimeDefault,
config_bundle: ConfigBundleRef {
id: bundle.metadata.id,
digest: bundle.metadata.digest,
},
initial_input: None,
}
}
async fn spawn_runtime_server() -> (Runtime, WorkerRef, String) {
let runtime = Runtime::new_memory();
let worker = runtime
.create_worker(CreateWorkerRequest::default())
let runtime =
Runtime::with_execution_backend(RuntimeOptions::default(), Arc::new(WsBackend))
.unwrap();
runtime
.store_config_bundle(ws_test_bundle(ProfileSelector::RuntimeDefault))
.unwrap();
let worker = runtime.create_worker(ws_create_request()).unwrap();
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn({
@ -1169,9 +1277,7 @@ mod ws_tests {
#[tokio::test]
async fn runtime_ws_cursor_resume_is_duplicate_safe_and_filters_workers() {
let (runtime, worker_ref, url) = spawn_runtime_server().await;
let other = runtime
.create_worker(CreateWorkerRequest::default())
.unwrap();
let other = runtime.create_worker(ws_create_request()).unwrap();
let first = runtime
.observe_worker_event(
&worker_ref,

View File

@ -24,4 +24,5 @@ mod runtime;
#[cfg(feature = "fs-store")]
pub use fs_store::{FsRuntimeStore, FsRuntimeStoreOptions};
pub use management::{RuntimeLimits, RuntimeOptions};
pub use runtime::Runtime;

View File

@ -4,15 +4,17 @@ use crate::catalog::{
};
use crate::config_bundle::{
ConfigBundle, ConfigBundleAvailability, ConfigBundleSummary, validate_config_bundle,
validate_config_bundle_ref, validate_profile_selector,
validate_config_bundle_ref,
};
use crate::diagnostics::{DiagnosticSeverity, RuntimeDiagnostic};
#[cfg(feature = "fs-store")]
use crate::diagnostics::DiagnosticSeverity;
use crate::diagnostics::RuntimeDiagnostic;
use crate::error::RuntimeError;
use crate::execution::{
WorkerExecutionBackend, WorkerExecutionBackendKind, WorkerExecutionBackendRef,
WorkerExecutionHandle, WorkerExecutionOperation, WorkerExecutionResult,
WorkerExecutionRunState, WorkerExecutionSpawnRequest, WorkerExecutionSpawnResult,
WorkerExecutionStatus,
WorkerExecutionBindingIdentity, WorkerExecutionHandle, WorkerExecutionOperation,
WorkerExecutionResult, WorkerExecutionRunState, WorkerExecutionSpawnRequest,
WorkerExecutionSpawnResult, WorkerExecutionStatus,
};
#[cfg(feature = "fs-store")]
use crate::fs_store::{
@ -231,55 +233,112 @@ impl Runtime {
Ok(event_id)
}
/// Create a Worker in the embedded catalog.
/// Create a Worker through the canonical ConfigBundle + execution backend path.
pub fn create_worker(
&self,
request: CreateWorkerRequest,
) -> Result<WorkerDetail, RuntimeError> {
let mut state = self.lock()?;
state.ensure_running()?;
validate_create_worker_request(&request)?;
state.validate_worker_config_boundary(&request)?;
let (backend, worker_ref, spawn_request) = {
let mut state = self.lock()?;
state.ensure_running()?;
validate_create_worker_request(&request)?;
state.validate_worker_config_boundary(&request)?;
let backend = state.execution_backend.clone().ok_or_else(|| {
RuntimeError::ExecutionBackendUnavailable {
message: "worker creation requires an execution backend".to_string(),
}
})?;
let worker_id = WorkerId::generated(state.next_worker_sequence);
state.next_worker_sequence += 1;
let worker_ref = WorkerRef::new(state.runtime_id.clone(), worker_id.clone());
let backend = state.execution_backend.clone();
let spawn_request = backend.as_ref().map(|_| WorkerExecutionSpawnRequest {
worker_ref: worker_ref.clone(),
request: request.clone(),
context: self.execution_context(worker_ref.clone()),
});
let event_id = state.push_event(
Some(worker_ref.clone()),
RuntimeEventKind::WorkerCreated,
format!("worker {worker_id} created"),
);
let worker_id = WorkerId::generated(state.next_worker_sequence);
state.next_worker_sequence += 1;
let worker_ref = WorkerRef::new(state.runtime_id.clone(), worker_id.clone());
let event_id = state.push_event(
Some(worker_ref.clone()),
RuntimeEventKind::WorkerCreated,
format!("worker {worker_id} created"),
);
let record = WorkerRecord {
worker_ref: worker_ref.clone(),
worker_id: worker_id.clone(),
status: WorkerStatus::Running,
request,
execution: WorkerExecutionStatus::unconnected(),
execution_handle: None,
transcript: Vec::new(),
next_transcript_sequence: 1,
last_event_id: event_id,
let mut transcript = Vec::new();
let mut next_transcript_sequence = 1;
if let Some(input) = request.initial_input.clone() {
transcript.push(TranscriptEntry {
sequence: next_transcript_sequence,
worker_ref: worker_ref.clone(),
role: TranscriptRole::User,
content: input.content,
event_id,
});
next_transcript_sequence += 1;
}
let record = WorkerRecord {
worker_ref: worker_ref.clone(),
worker_id: worker_id.clone(),
status: WorkerStatus::Running,
request: request.clone(),
execution: WorkerExecutionStatus::unconnected(),
execution_handle: None,
transcript,
next_transcript_sequence,
last_event_id: event_id,
};
state.workers.insert(worker_id, record);
let spawn_request = WorkerExecutionSpawnRequest {
worker_ref: worker_ref.clone(),
request,
context: self.execution_context(worker_ref.clone()),
};
(backend, worker_ref, spawn_request)
};
let detail = record.detail(&state.runtime_id);
state.emit_create_diagnostics(&detail);
state.workers.insert(worker_id.clone(), record);
state.persist_runtime_snapshot()?;
state.persist_worker(&worker_id)?;
state.persist_event_by_id(event_id)?;
drop(state);
if let (Some(backend), Some(spawn_request)) = (backend, spawn_request) {
let result = backend.spawn_worker(spawn_request);
self.apply_spawn_result(&worker_ref, result)
let spawn_result = backend.spawn_worker(spawn_request);
let (handle, run_state) = match spawn_result {
WorkerExecutionSpawnResult::Connected { handle, run_state } => (handle, run_state),
WorkerExecutionSpawnResult::Rejected(result)
| WorkerExecutionSpawnResult::Errored(result) => {
self.rollback_failed_create(&worker_ref)?;
return Err(RuntimeError::WorkerExecutionRejected {
worker_id: worker_ref.worker_id.clone(),
operation: result.operation,
outcome: result.outcome,
message: result.message_or_default(),
result,
});
}
};
if let Some(initial_input) = {
let state = self.lock()?;
state.worker(&worker_ref)?.request.initial_input.clone()
} {
let dispatch_result = backend.dispatch_input(&handle, initial_input);
if !dispatch_result.is_accepted() {
let _ = backend.stop_worker(&handle);
self.rollback_failed_create(&worker_ref)?;
return Err(RuntimeError::WorkerExecutionRejected {
worker_id: worker_ref.worker_id.clone(),
operation: dispatch_result.operation,
outcome: dispatch_result.outcome,
message: dispatch_result.message_or_default(),
result: dispatch_result,
});
}
self.commit_created_worker(
&worker_ref,
handle,
WorkerExecutionRunState::Busy,
WorkerExecutionResult::accepted(
WorkerExecutionOperation::Input,
WorkerExecutionRunState::Busy,
),
)
} else {
Ok(detail)
self.commit_created_worker(
&worker_ref,
handle,
run_state,
WorkerExecutionResult::accepted(WorkerExecutionOperation::Spawn, run_state),
)
}
}
@ -328,7 +387,9 @@ impl Runtime {
"worker has no execution backend",
);
let worker = state.worker_mut(worker_ref)?;
worker.execution = WorkerExecutionStatus::unconnected().with_result(result);
let mut execution = WorkerExecutionStatus::unconnected().with_result(result);
execution.binding = worker.execution.binding.clone();
worker.execution = execution;
state.persist_worker(&worker_ref.worker_id)?;
return Err(RuntimeError::WorkerExecutionUnavailable {
worker_id: worker_ref.worker_id.clone(),
@ -370,6 +431,7 @@ impl Runtime {
worker.execution = WorkerExecutionStatus {
backend: WorkerExecutionBackendKind::Connected,
run_state: dispatch_result.run_state,
binding: worker.execution.binding.clone(),
last_result: Some(dispatch_result),
};
worker.transcript.push(TranscriptEntry {
@ -414,38 +476,44 @@ impl Runtime {
})
}
fn apply_spawn_result(
fn commit_created_worker(
&self,
worker_ref: &WorkerRef,
result: WorkerExecutionSpawnResult,
handle: WorkerExecutionHandle,
run_state: WorkerExecutionRunState,
result: WorkerExecutionResult,
) -> Result<WorkerDetail, RuntimeError> {
let mut state = self.lock()?;
let runtime_id = state.runtime_id.clone();
let detail = {
let binding = WorkerExecutionBindingIdentity::from_handle(&handle);
let worker = state.worker_mut(worker_ref)?;
match result {
WorkerExecutionSpawnResult::Connected { handle, run_state } => {
worker.execution_handle = Some(handle);
worker.execution = WorkerExecutionStatus::connected(run_state).with_result(
WorkerExecutionResult::accepted(WorkerExecutionOperation::Spawn, run_state),
);
}
WorkerExecutionSpawnResult::Rejected(result)
| WorkerExecutionSpawnResult::Errored(result) => {
worker.execution_handle = None;
worker.execution = WorkerExecutionStatus {
backend: WorkerExecutionBackendKind::Connected,
run_state: result.run_state,
last_result: Some(result),
};
}
}
worker.execution_handle = Some(handle);
worker.execution = WorkerExecutionStatus::connected(run_state)
.with_binding(binding)
.with_result(result);
worker.detail(&runtime_id)
};
state.persist_runtime_snapshot()?;
state.persist_worker(&worker_ref.worker_id)?;
let worker = state.worker(worker_ref)?;
for entry in &worker.transcript {
state.persist_transcript_entry(&worker_ref.worker_id, entry.sequence)?;
}
state.persist_event_by_id(detail.last_event_id)?;
Ok(detail)
}
fn rollback_failed_create(&self, worker_ref: &WorkerRef) -> Result<(), RuntimeError> {
let mut state = self.lock()?;
if let Some(record) = state.workers.remove(&worker_ref.worker_id) {
state.events.retain(|event| {
event.id != record.last_event_id || event.worker_ref.as_ref() != Some(worker_ref)
});
}
Ok(())
}
fn record_execution_result(
&self,
worker_ref: &WorkerRef,
@ -456,6 +524,7 @@ impl Runtime {
worker.execution = WorkerExecutionStatus {
backend: WorkerExecutionBackendKind::Connected,
run_state: result.run_state,
binding: worker.execution.binding.clone(),
last_result: Some(result),
};
state.persist_worker(&worker_ref.worker_id)?;
@ -827,6 +896,7 @@ struct RuntimeState {
execution_backend: Option<WorkerExecutionBackendRef>,
next_worker_sequence: u64,
next_event_id: u64,
#[cfg(feature = "fs-store")]
next_diagnostic_id: u64,
workers: BTreeMap<WorkerId, WorkerRecord>,
config_bundles: BTreeMap<String, ConfigBundle>,
@ -852,6 +922,7 @@ impl RuntimeState {
execution_backend: None,
next_worker_sequence: 1,
next_event_id: 1,
#[cfg(feature = "fs-store")]
next_diagnostic_id: 1,
workers: BTreeMap::new(),
config_bundles: BTreeMap::new(),
@ -883,6 +954,7 @@ impl RuntimeState {
execution_backend: None,
next_worker_sequence: 1,
next_event_id: 1,
#[cfg(feature = "fs-store")]
next_diagnostic_id: 1,
workers: BTreeMap::new(),
config_bundles: BTreeMap::new(),
@ -915,7 +987,28 @@ impl RuntimeState {
}
let mut workers = BTreeMap::new();
let mut diagnostics = persisted.diagnostics;
let mut next_diagnostic_id = persisted.next_diagnostic_id;
for (worker_id, worker) in persisted.workers {
let execution = if worker.execution.binding.is_some()
&& worker.execution.backend == WorkerExecutionBackendKind::Connected
{
let stale = WorkerExecutionStatus::stale(worker.execution);
diagnostics.push(RuntimeDiagnostic {
id: next_diagnostic_id,
severity: DiagnosticSeverity::Warning,
code: "worker_execution_mapping_stale".to_string(),
message: format!(
"worker {} has persisted execution binding identity but no live execution handle was restored",
worker.worker_id
),
worker_ref: Some(worker.worker_ref.clone()),
});
next_diagnostic_id += 1;
stale
} else {
worker.execution
};
workers.insert(
worker_id,
WorkerRecord {
@ -923,7 +1016,7 @@ impl RuntimeState {
worker_id: worker.worker_id,
status: worker.status,
request: worker.request,
execution: WorkerExecutionStatus::unconnected(),
execution,
execution_handle: None,
transcript: worker.transcript,
next_transcript_sequence: worker.next_transcript_sequence,
@ -942,11 +1035,11 @@ impl RuntimeState {
execution_backend: None,
next_worker_sequence: persisted.next_worker_sequence,
next_event_id: persisted.next_event_id,
next_diagnostic_id: persisted.next_diagnostic_id,
next_diagnostic_id,
workers,
config_bundles: persisted.config_bundles,
events: persisted.events,
diagnostics: persisted.diagnostics,
diagnostics,
#[cfg(feature = "ws-server")]
next_observation_sequence: 1,
#[cfg(feature = "ws-server")]
@ -1131,36 +1224,22 @@ impl RuntimeState {
&self,
request: &CreateWorkerRequest,
) -> Result<(), RuntimeError> {
match &request.config_bundle {
Some(reference) => {
let availability = self.check_config_bundle_ref(reference)?;
let bundle = self
.config_bundles
.get(&availability.reference.id)
.ok_or_else(|| RuntimeError::ConfigBundleMissing {
bundle_id: availability.reference.id.clone(),
})?;
if !bundle.contains_profile(&request.profile) {
return Err(RuntimeError::InvalidProfileSelector {
profile: profile_label(&request.profile),
bundle_id: Some(reference.id.clone()),
message: "profile selector is not declared by synced config bundle"
.to_string(),
});
}
Ok(())
}
None => match &request.profile {
ProfileSelector::RuntimeDefault | ProfileSelector::Builtin(_) => {
validate_profile_selector(request.profile.clone(), None)
}
ProfileSelector::Named(_) => Err(RuntimeError::InvalidProfileSelector {
profile: profile_label(&request.profile),
bundle_id: None,
message: "named profiles require a synced config bundle reference".to_string(),
}),
},
let reference = &request.config_bundle;
let availability = self.check_config_bundle_ref(reference)?;
let bundle = self
.config_bundles
.get(&availability.reference.id)
.ok_or_else(|| RuntimeError::ConfigBundleMissing {
bundle_id: availability.reference.id.clone(),
})?;
if !bundle.contains_profile(&request.profile) {
return Err(RuntimeError::InvalidProfileSelector {
profile: profile_label(&request.profile),
bundle_id: Some(reference.id.clone()),
message: "profile selector is not declared by synced config bundle".to_string(),
});
}
Ok(())
}
fn ensure_worker_ref(&self, worker_ref: &WorkerRef) -> Result<(), RuntimeError> {
@ -1373,43 +1452,6 @@ impl RuntimeState {
status.run_state = next_run_state;
true
}
fn push_diagnostic(
&mut self,
severity: DiagnosticSeverity,
code: impl Into<String>,
message: impl Into<String>,
worker_ref: Option<WorkerRef>,
) {
let id = self.next_diagnostic_id;
self.next_diagnostic_id += 1;
self.diagnostics.push(RuntimeDiagnostic {
id,
severity,
code: code.into(),
message: message.into(),
worker_ref,
});
}
fn emit_create_diagnostics(&mut self, detail: &WorkerDetail) {
if detail.config_bundle.is_none() {
self.push_diagnostic(
DiagnosticSeverity::Info,
"runtime.local_default_resources",
"worker created without ConfigBundleRef; runtime-local defaults are assumed",
Some(detail.worker_ref.clone()),
);
}
if detail.requested_capabilities.is_empty() {
self.push_diagnostic(
DiagnosticSeverity::Info,
"worker.tools_less",
"worker created without requested tool capabilities",
Some(detail.worker_ref.clone()),
);
}
}
}
#[derive(Debug)]
@ -1433,10 +1475,8 @@ impl WorkerRecord {
worker_id: self.worker_id.clone(),
status: self.status,
execution: self.execution.clone(),
intent: self.request.intent.clone(),
profile: self.request.profile.clone(),
requested_capability_count: self.request.requested_capabilities.len(),
has_config_bundle: self.request.config_bundle.is_some(),
config_bundle: self.request.config_bundle.clone(),
transcript_len: self.transcript.len(),
last_event_id: self.last_event_id,
}
@ -1449,12 +1489,8 @@ impl WorkerRecord {
worker_id: self.worker_id.clone(),
status: self.status,
execution: self.execution.clone(),
intent: self.request.intent.clone(),
profile: self.request.profile.clone(),
config_bundle: self.request.config_bundle.clone(),
requested_capabilities: self.request.requested_capabilities.clone(),
workspace_refs: self.request.workspace_refs.clone(),
mount_refs: self.request.mount_refs.clone(),
transcript_len: self.transcript.len(),
last_event_id: self.last_event_id,
}
@ -1467,6 +1503,7 @@ impl WorkerRecord {
worker_id: self.worker_id.clone(),
status: self.status,
request: self.request.clone(),
execution: self.execution.clone(),
transcript: self.transcript.clone(),
next_transcript_sequence: self.next_transcript_sequence,
last_event_id: self.last_event_id,
@ -1483,17 +1520,25 @@ fn profile_label(selector: &ProfileSelector) -> String {
}
fn validate_create_worker_request(request: &CreateWorkerRequest) -> Result<(), RuntimeError> {
if let crate::catalog::WorkerIntent::Task { objective } = &request.intent {
if objective.trim().is_empty() {
return Err(RuntimeError::InvalidRequest(
"task objective must not be empty".to_string(),
));
}
if request.config_bundle.id.trim().is_empty() {
return Err(RuntimeError::InvalidRequest(
"config_bundle.id must not be empty".to_string(),
));
}
for capability in &request.requested_capabilities {
if capability.name.trim().is_empty() {
if request.config_bundle.digest.trim().is_empty() {
return Err(RuntimeError::InvalidRequest(
"config_bundle.digest must not be empty".to_string(),
));
}
if let Some(input) = &request.initial_input {
if input.kind != WorkerInputKind::User {
return Err(RuntimeError::InvalidInitialInputKind {
kind: format!("{:?}", input.kind),
});
}
if input.content.trim().is_empty() {
return Err(RuntimeError::InvalidRequest(
"capability name must not be empty".to_string(),
"initial_input.content must not be empty".to_string(),
));
}
}
@ -1512,7 +1557,7 @@ fn validate_worker_input(input: &WorkerInput) -> Result<(), RuntimeError> {
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog::{CapabilityRequest, ConfigBundleRef, ProfileSelector, WorkerIntent};
use crate::catalog::{ConfigBundleRef, ProfileSelector};
use crate::config_bundle::{
ConfigBundle, ConfigBundleMetadata, ConfigBundleProvenance, ConfigDeclaration,
ConfigDeclarationKind, ConfigProfileDescriptor,
@ -1524,19 +1569,45 @@ mod tests {
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
fn task_request(objective: &str) -> CreateWorkerRequest {
fn task_request(_objective: &str) -> CreateWorkerRequest {
let profile = ProfileSelector::Builtin("builtin:coder".to_string());
let bundle = test_bundle_for_profile(profile.clone());
CreateWorkerRequest {
intent: WorkerIntent::Task {
objective: objective.to_string(),
profile,
config_bundle: ConfigBundleRef {
id: bundle.metadata.id,
digest: bundle.metadata.digest,
},
profile: ProfileSelector::Builtin("builtin:coder".to_string()),
config_bundle: None,
requested_capabilities: vec![CapabilityRequest::named("read")],
workspace_refs: Vec::new(),
mount_refs: Vec::new(),
initial_input: None,
}
}
fn test_bundle_for_profile(profile: ProfileSelector) -> ConfigBundle {
ConfigBundle {
metadata: ConfigBundleMetadata {
id: "bundle-1".to_string(),
digest: String::new(),
revision: "rev-1".to_string(),
workspace_id: "workspace-1".to_string(),
created_at: "2026-06-26T00:00:00Z".to_string(),
provenance: ConfigBundleProvenance {
source: "workspace-backend".to_string(),
detail: Some("profile-sync".to_string()),
},
},
profiles: vec![ConfigProfileDescriptor {
selector: profile,
label: Some("Coder".to_string()),
}],
declarations: vec![ConfigDeclaration {
kind: ConfigDeclarationKind::CapabilityGrant,
name: "read".to_string(),
reference: "capability:read".to_string(),
}],
}
.with_computed_digest()
}
#[derive(Default)]
struct TestExecutionBackend {
dispatch_result: Mutex<Option<WorkerExecutionResult>>,
@ -1609,77 +1680,58 @@ mod tests {
}
fn runtime_with_backend() -> Runtime {
Runtime::with_execution_backend(
let runtime = Runtime::with_execution_backend(
RuntimeOptions::default(),
Arc::new(TestExecutionBackend::default()),
)
.unwrap()
.unwrap();
runtime.store_config_bundle(test_bundle()).unwrap();
runtime
}
fn runtime_and_backend() -> (Runtime, Arc<TestExecutionBackend>) {
let backend = Arc::new(TestExecutionBackend::default());
let runtime =
Runtime::with_execution_backend(RuntimeOptions::default(), backend.clone()).unwrap();
runtime.store_config_bundle(test_bundle()).unwrap();
(runtime, backend)
}
fn test_bundle() -> ConfigBundle {
ConfigBundle {
metadata: ConfigBundleMetadata {
id: "bundle-1".to_string(),
digest: String::new(),
revision: "rev-1".to_string(),
workspace_id: "workspace-1".to_string(),
created_at: "2026-06-26T00:00:00Z".to_string(),
provenance: ConfigBundleProvenance {
source: "workspace-backend".to_string(),
detail: Some("profile-sync".to_string()),
},
},
profiles: vec![ConfigProfileDescriptor {
selector: ProfileSelector::Builtin("builtin:coder".to_string()),
label: Some("Coder".to_string()),
}],
declarations: vec![ConfigDeclaration {
kind: ConfigDeclarationKind::CapabilityGrant,
name: "read".to_string(),
reference: "capability:read".to_string(),
}],
}
.with_computed_digest()
test_bundle_for_profile(ProfileSelector::Builtin("builtin:coder".to_string()))
}
fn bundled_task_request(objective: &str, bundle: &ConfigBundle) -> CreateWorkerRequest {
let mut request = task_request(objective);
request.config_bundle = Some(ConfigBundleRef {
request.config_bundle = ConfigBundleRef {
id: bundle.metadata.id.clone(),
digest: bundle.metadata.digest.clone(),
});
};
request
}
#[test]
fn create_list_and_detail_preserve_runtime_worker_authority() {
let runtime = Runtime::new_memory();
let runtime = runtime_with_backend();
let detail = runtime.create_worker(task_request("implement v0")).unwrap();
assert_eq!(detail.worker_ref.runtime_id, runtime.runtime_id().unwrap());
assert_eq!(detail.status, WorkerStatus::Running);
assert!(detail.config_bundle.is_none());
assert_eq!(detail.config_bundle.id, "bundle-1");
let list = runtime.list_workers().unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0].worker_ref, detail.worker_ref);
assert_eq!(list[0].requested_capability_count, 1);
assert_eq!(list[0].config_bundle, detail.config_bundle);
let fetched = runtime.worker_detail(&detail.worker_ref).unwrap();
assert_eq!(fetched.worker_id, detail.worker_id);
assert_eq!(fetched.intent, detail.intent);
assert_eq!(fetched.profile, detail.profile);
}
#[test]
fn synced_config_bundle_is_stored_checked_and_used_for_worker_creation() {
let runtime = Runtime::new_memory();
let runtime = runtime_with_backend();
let bundle = test_bundle();
let availability = runtime.store_config_bundle(bundle.clone()).unwrap();
assert_eq!(availability.reference.id, "bundle-1");
@ -1697,7 +1749,7 @@ mod tests {
let detail = runtime
.create_worker(bundled_task_request("synced", &bundle))
.unwrap();
assert_eq!(detail.config_bundle, Some(availability.reference));
assert_eq!(detail.config_bundle, availability.reference);
}
#[test]
@ -1746,8 +1798,8 @@ mod tests {
#[test]
fn rejects_worker_refs_from_another_runtime() {
let runtime_a = Runtime::new_memory();
let runtime_b = Runtime::new_memory();
let runtime_a = runtime_with_backend();
let runtime_b = runtime_with_backend();
let detail = runtime_a.create_worker(task_request("runtime a")).unwrap();
let err = runtime_b.worker_detail(&detail.worker_ref).unwrap_err();
@ -1755,52 +1807,50 @@ mod tests {
}
#[test]
fn tools_less_worker_without_config_bundle_uses_local_defaults_and_diagnostics() {
let runtime = Runtime::new_memory();
let detail = runtime
.create_worker(CreateWorkerRequest::tools_less(
WorkerIntent::default(),
ProfileSelector::RuntimeDefault,
))
.unwrap();
fn create_worker_rejects_system_initial_input_without_persisting_worker() {
let runtime = runtime_with_backend();
let mut request = task_request("system initial input");
request.initial_input = Some(WorkerInput::system("role/system belongs in config bundle"));
assert!(detail.config_bundle.is_none());
assert!(detail.requested_capabilities.is_empty());
let diagnostics = runtime.diagnostics().unwrap();
assert_eq!(diagnostics.len(), 2);
let error = runtime.create_worker(request).unwrap_err();
assert!(matches!(
error,
RuntimeError::InvalidInitialInputKind { .. }
));
assert!(runtime.list_workers().unwrap().is_empty());
let events = runtime
.read_events(&runtime.event_cursor_from_start().unwrap(), 16)
.unwrap();
assert!(
diagnostics
events
.events
.iter()
.any(|diagnostic| diagnostic.code == "runtime.local_default_resources")
);
assert!(
diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "worker.tools_less")
.all(|event| event.kind != RuntimeEventKind::WorkerCreated)
);
}
#[test]
fn backend_unconnected_worker_input_is_rejected_and_not_transcribed() {
fn create_worker_without_execution_backend_is_rejected_and_not_persisted() {
let runtime = Runtime::new_memory();
let detail = runtime.create_worker(task_request("placeholder")).unwrap();
assert_eq!(
detail.execution.backend,
WorkerExecutionBackendKind::Unconnected
);
let err = runtime
.send_input(&detail.worker_ref, WorkerInput::user("must reject"))
runtime.store_config_bundle(test_bundle()).unwrap();
let error = runtime
.create_worker(task_request("no backend"))
.unwrap_err();
assert!(matches!(
err,
RuntimeError::WorkerExecutionUnavailable { .. }
error,
RuntimeError::ExecutionBackendUnavailable { .. }
));
assert!(runtime.list_workers().unwrap().is_empty());
}
let projection = runtime
.transcript_projection(&detail.worker_ref, TranscriptQuery::new(0, 1))
.unwrap();
assert_eq!(projection.total_items, 0);
#[test]
fn create_worker_missing_config_bundle_is_rejected_before_backend() {
let runtime = Runtime::new_memory();
let error = runtime
.create_worker(task_request("missing bundle"))
.unwrap_err();
assert!(matches!(error, RuntimeError::ConfigBundleMissing { .. }));
assert!(runtime.list_workers().unwrap().is_empty());
}
#[test]
@ -1885,6 +1935,7 @@ mod tests {
let runtime =
Runtime::with_execution_backend(RuntimeOptions::default(), Arc::new(InputOnlyBackend))
.unwrap();
runtime.store_config_bundle(test_bundle()).unwrap();
let detail = runtime.create_worker(task_request("no stop")).unwrap();
let err = runtime
@ -1916,6 +1967,7 @@ mod tests {
Arc::new(TestExecutionBackend::default()),
)
.unwrap();
runtime.store_config_bundle(test_bundle()).unwrap();
let detail = runtime.create_worker(task_request("chat")).unwrap();
let first = runtime
@ -1946,7 +1998,7 @@ mod tests {
#[test]
fn stop_and_cancel_workers_update_summary() {
let runtime = Runtime::new_memory();
let runtime = runtime_with_backend();
let stopped = runtime.create_worker(task_request("stop me")).unwrap();
let cancelled = runtime.create_worker(task_request("cancel me")).unwrap();
@ -1969,7 +2021,7 @@ mod tests {
#[test]
fn stop_then_cancel_preserves_stopped_terminal_state() {
let runtime = Runtime::new_memory();
let runtime = runtime_with_backend();
let cursor = runtime.event_cursor_from_start().unwrap();
let worker = runtime
.create_worker(task_request("stable stopped"))
@ -2014,7 +2066,7 @@ mod tests {
#[test]
fn cancel_then_stop_preserves_cancelled_terminal_state() {
let runtime = Runtime::new_memory();
let runtime = runtime_with_backend();
let cursor = runtime.event_cursor_from_start().unwrap();
let worker = runtime
.create_worker(task_request("stable cancelled"))
@ -2126,6 +2178,7 @@ mod tests {
runtime.summary().unwrap().backend,
RuntimeBackendKind::FsStore
);
runtime.store_config_bundle(test_bundle()).unwrap();
let worker = runtime.create_worker(task_request("persist me")).unwrap();
runtime
@ -2149,6 +2202,28 @@ mod tests {
.unwrap();
let restored_worker = restored.worker_detail(&worker.worker_ref).unwrap();
assert_eq!(restored_worker.status, WorkerStatus::Stopped);
assert_eq!(
restored_worker.execution.backend,
WorkerExecutionBackendKind::Stale
);
assert_eq!(
restored_worker
.execution
.binding
.as_ref()
.map(|binding| binding.backend_id.as_str()),
Some("test-execution-backend")
);
assert!(
restored
.diagnostics()
.unwrap()
.iter()
.any(
|diagnostic| diagnostic.code == "worker_execution_mapping_stale"
&& diagnostic.worker_ref.as_ref() == Some(&worker.worker_ref)
)
);
assert_eq!(restored_worker.transcript_len, 2);
let projection = restored
@ -2224,13 +2299,17 @@ mod tests {
let missing_root = fs_store_root("missing");
let missing_runtime_id = RuntimeId::new("runtime-missing").unwrap();
let missing_runtime = Runtime::with_fs_store(crate::fs_store::FsRuntimeStoreOptions {
root: missing_root.clone(),
runtime_id: Some(missing_runtime_id.clone()),
display_name: None,
limits: RuntimeLimits::default(),
})
let missing_runtime = Runtime::with_fs_store_and_execution_backend(
crate::fs_store::FsRuntimeStoreOptions {
root: missing_root.clone(),
runtime_id: Some(missing_runtime_id.clone()),
display_name: None,
limits: RuntimeLimits::default(),
},
Arc::new(TestExecutionBackend::default()),
)
.unwrap();
missing_runtime.store_config_bundle(test_bundle()).unwrap();
missing_runtime
.create_worker(task_request("missing worker snapshot"))
.unwrap();

View File

@ -14,13 +14,11 @@ async-trait = { workspace = true }
clap = { version = "4.6.0", features = ["derive"] }
llm-engine = { workspace = true }
session-store = { workspace = true }
pod-store = { workspace = true }
manifest = { workspace = true }
mcp = { workspace = true }
protocol = { workspace = true }
provider = { workspace = true }
client = { workspace = true }
pod-registry = { workspace = true }
worker-runtime = { workspace = true, features = ["ws-server"], optional = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }

View File

@ -10,7 +10,7 @@ Owns:
- Worker lifecycle and socket protocol serving
- Engine construction around a resolved Manifest
- session-store and pod-store coordination
- session-store and session-store worker metadata coordination
- built-in tool registration under scope/policy
- spawned-child orchestration hooks
@ -19,7 +19,7 @@ Does not own:
- provider-specific wire formats (`provider` / `llm-engine` clients)
- product CLI parsing (`yoi`)
- TUI display authority (`tui`)
- current-state storage schema outside Worker metadata (`pod-store`)
- current-state storage schema outside Worker metadata (`session-store` worker metadata)
## Design notes

View File

@ -11,8 +11,8 @@
//! cargo run -p worker --example worker_cli
//! ```
use pod_store::{CombinedStore, FsWorkerStore};
use session_store::FsStore;
use session_store::{CombinedStore, FsWorkerStore};
use worker::{Worker, WorkerManifest, WorkerRunResult};
fn manifest_toml(pwd: &std::path::Path) -> String {

View File

@ -5,8 +5,8 @@
//! cargo run -p worker --example worker_protocol
//! ```
use pod_store::{CombinedStore, FsWorkerStore};
use session_store::FsStore;
use session_store::{CombinedStore, FsWorkerStore};
use worker::{Event, Method, WorkerController};
fn manifest_toml(pwd: &std::path::Path) -> String {

View File

@ -5,8 +5,8 @@ use std::sync::atomic::Ordering;
use llm_engine::EngineError;
use llm_engine::llm_client::client::LlmClient;
use manifest::TicketFeatureAccessConfig;
use pod_store::WorkerMetadataStore;
use session_store::Store;
use session_store::WorkerMetadataStore;
use ticket::LocalTicketBackend;
use ticket::config::TicketConfig;
use tokio::sync::{broadcast, mpsc, oneshot};
@ -608,7 +608,7 @@ where
let spawner_name = worker.manifest().worker.name.clone();
let spawner_manifest = worker.manifest().clone();
let prompts = worker.prompts().clone();
let pod_store = worker.store().clone();
let worker_metadata_store = worker.store().clone();
let self_parent_socket = worker.callback_socket().cloned();
// The Worker's SharedScope (already augmented with the bash-output
@ -724,8 +724,13 @@ where
worker.register_tool(send_to_worker_tool(spawned_registry.clone()));
worker.register_tool(read_worker_output_tool(spawned_registry.clone()));
worker.register_tool(stop_worker_tool(spawned_registry.clone()));
let discovery =
WorkerDiscovery::new(pod_store, spawner_name, runtime_base, cwd, spawned_registry);
let discovery = WorkerDiscovery::new(
worker_metadata_store,
spawner_name,
runtime_base,
cwd,
spawned_registry,
);
worker.register_tool(list_workers_tool(discovery.clone()));
worker.register_tool(restore_worker_tool(discovery.clone()));
worker.register_tool(send_to_peer_worker_tool(discovery));

View File

@ -18,19 +18,19 @@ use async_trait::async_trait;
use client::WorkerRuntimeCommand;
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use manifest::{Permission, ScopeRule};
use pod_store::{
WorkerActiveSegmentRef, WorkerMetadata, WorkerMetadataStore, validate_worker_name,
};
use protocol::stream::JsonLineReader;
use protocol::{Event, Method, WorkerStatus};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use session_store::{SegmentId, SessionId};
use session_store::{
WorkerActiveSegmentRef, WorkerMetadata, WorkerMetadataStore, validate_worker_name,
};
use tokio::net::UnixStream;
use tokio::process::Command;
use crate::runtime::dir::SpawnedWorkerRecord;
use crate::runtime::pod_registry;
use crate::runtime::worker_allocation;
use crate::spawn::comm_tools::connect_and_send;
use crate::spawn::registry::SpawnedWorkerRegistry;
@ -705,9 +705,9 @@ pub enum WorkerDiscoveryError {
#[error("session store error: {0}")]
Store(#[from] session_store::StoreError),
#[error("worker store error: {0}")]
WorkerStore(#[from] pod_store::WorkerStoreError),
WorkerStore(#[from] session_store::WorkerStoreError),
#[error("scope lock error: {0}")]
ScopeLock(#[from] pod_registry::ScopeLockError),
ScopeLock(#[from] worker_allocation::ScopeLockError),
#[error("failed to launch restore process: {0}")]
RestoreSpawn(io::Error),
#[error("failed to launch restore runtime command `{command}`: {source}")]
@ -748,7 +748,7 @@ impl VisibilitySet {
}
}
fn comm_info_from_spawned_child(child: &pod_store::WorkerSpawnedChild) -> CommRegistryInfo {
fn comm_info_from_spawned_child(child: &session_store::WorkerSpawnedChild) -> CommRegistryInfo {
let scope_delegated = child
.scope_delegated
.iter()
@ -773,7 +773,7 @@ fn comm_info_from_spawned_child(child: &pod_store::WorkerSpawnedChild) -> CommRe
}
async fn summarize_spawned_children(
children: &[pod_store::WorkerSpawnedChild],
children: &[session_store::WorkerSpawnedChild],
) -> SpawnedChildrenSummary {
let mut summary = SpawnedChildrenSummary {
count: children.len(),
@ -832,8 +832,8 @@ async fn probe_socket(socket_path: &Path) -> LiveInfo {
fn lookup_segment_lock(
segment_id: SegmentId,
) -> Result<Option<pod_registry::SegmentLockInfo>, pod_registry::ScopeLockError> {
pod_registry::lookup_segment(segment_id)
) -> Result<Option<worker_allocation::SegmentLockInfo>, worker_allocation::ScopeLockError> {
worker_allocation::lookup_segment(segment_id)
}
#[derive(Debug, Deserialize, JsonSchema)]
@ -1061,9 +1061,11 @@ mod tests {
use std::sync::Mutex;
use manifest::{Permission, ScopeRule};
use pod_store::{FsWorkerStore, WorkerSpawnedChild, WorkerSpawnedScopeRule, WorkerStoreError};
use protocol::stream::JsonLineWriter;
use protocol::{Alert, AlertLevel, AlertSource};
use session_store::{
FsWorkerStore, WorkerSpawnedChild, WorkerSpawnedScopeRule, WorkerStoreError,
};
use session_store::{new_segment_id, new_session_id};
use tempfile::TempDir;
use tokio::net::UnixListener;
@ -1143,7 +1145,7 @@ mod tests {
child("child-pending", &pending_socket),
],
reclaimed_children: Vec::new(),
peers: vec![pod_store::WorkerPeer {
peers: vec![session_store::WorkerPeer {
worker_name: "peer".into(),
}],
resolved_manifest_snapshot: None,
@ -1209,7 +1211,7 @@ mod tests {
workspace_root: None,
spawned_children: Vec::new(),
reclaimed_children: Vec::new(),
peers: vec![pod_store::WorkerPeer {
peers: vec![session_store::WorkerPeer {
worker_name: "parent".into(),
}],
resolved_manifest_snapshot: None,
@ -1317,7 +1319,7 @@ mod tests {
assert!(matches!(restore_plan, RestorePlan::Restore { .. }));
let lock_socket = runtime_base.join("lock-owner.sock");
let _guard = pod_registry::install_top_level(
let _guard = worker_allocation::install_top_level(
"lock-owner".into(),
std::process::id(),
lock_socket.clone(),
@ -1415,7 +1417,7 @@ mod tests {
workspace_root: None,
spawned_children: Vec::new(),
reclaimed_children: Vec::new(),
peers: vec![pod_store::WorkerPeer {
peers: vec![session_store::WorkerPeer {
worker_name: "target".into(),
}],
resolved_manifest_snapshot: None,
@ -1455,7 +1457,7 @@ mod tests {
workspace_root: None,
spawned_children: Vec::new(),
reclaimed_children: Vec::new(),
peers: vec![pod_store::WorkerPeer {
peers: vec![session_store::WorkerPeer {
worker_name: "target".into(),
}],
resolved_manifest_snapshot: None,
@ -1468,7 +1470,7 @@ mod tests {
workspace_root: None,
spawned_children: Vec::new(),
reclaimed_children: Vec::new(),
peers: vec![pod_store::WorkerPeer {
peers: vec![session_store::WorkerPeer {
worker_name: "source".into(),
}],
resolved_manifest_snapshot: None,
@ -1573,7 +1575,7 @@ mod tests {
workspace_root: None,
spawned_children: Vec::new(),
reclaimed_children: Vec::new(),
peers: vec![pod_store::WorkerPeer {
peers: vec![session_store::WorkerPeer {
worker_name: "target".into(),
}],
resolved_manifest_snapshot: None,
@ -1586,7 +1588,7 @@ mod tests {
workspace_root: None,
spawned_children: Vec::new(),
reclaimed_children: Vec::new(),
peers: vec![pod_store::WorkerPeer {
peers: vec![session_store::WorkerPeer {
worker_name: "source".into(),
}],
resolved_manifest_snapshot: None,

View File

@ -9,7 +9,7 @@ use manifest::{
WorkerManifest, WorkerManifestConfig, paths,
plugin::{PluginDiscoveryOptions, resolve_plugin_config_for_startup},
};
use pod_store::{CombinedStore, FsWorkerStore, WorkerMetadataStore};
use session_store::{CombinedStore, FsWorkerStore, WorkerMetadataStore};
use session_store::{FsStore, SegmentId, Store};
use ticket::config::TicketRole;
@ -85,7 +85,7 @@ struct Cli {
/// Restore a Worker from an existing session. The Worker re-uses the
/// given session id and appends new turns to the same jsonl;
/// concurrent writers are prevented by the pod-registry.
/// concurrent writers are prevented by the worker-allocation.
/// Mutually exclusive with `--adopt` (spawned children always start
/// fresh).
#[arg(long, value_name = "UUID", conflicts_with_all = ["adopt"])]
@ -484,21 +484,21 @@ async fn run_cli_inner(cli: Cli) -> ExitCode {
return ExitCode::FAILURE;
}
};
let pod_store_dir = match paths::data_dir() {
Some(data_dir) => data_dir.join("pods"),
let worker_metadata_dir = match paths::data_dir() {
Some(data_dir) => data_dir.join("workers"),
None => store_dir
.parent()
.map(|parent| parent.join("pods"))
.map(|parent| parent.join("workers"))
.unwrap_or_else(|| PathBuf::from("workers")),
};
let pod_store = match FsWorkerStore::new(&pod_store_dir) {
let worker_metadata_store = match FsWorkerStore::new(&worker_metadata_dir) {
Ok(s) => s,
Err(e) => {
eprintln!("error: failed to initialize worker store at {pod_store_dir:?}: {e}");
eprintln!("error: failed to initialize worker store at {worker_metadata_dir:?}: {e}");
return ExitCode::FAILURE;
}
};
let store = CombinedStore::new(session_store, pod_store);
let store = CombinedStore::new(session_store, worker_metadata_store);
let mut worker = if cli.adopt {
let callback = match cli.callback.clone() {

View File

@ -10,7 +10,7 @@
//! parent's notification buffer. Control-plane-only variants may still have
//! a renderer for diagnostics, but receive-side classification keeps them
//! out of LLM history/context.
//! - **Apply side effects** on the parent (registry / pod-registry
//! - **Apply side effects** on the parent (registry / worker-allocation
//! updates) so that the receive path is idempotent and tolerant of
//! out-of-order delivery.
//!

View File

@ -1,2 +1,2 @@
pub mod dir;
pub use ::pod_registry;
pub mod worker_allocation;

View File

@ -0,0 +1,29 @@
//! Process-local Worker allocation table used only for scope ownership checks.
//!
//! This module is intentionally not a runtime identity store. Runtime Worker
//! identity, creation and durable persistence remain owned by worker-runtime
//! fs-store plus its execution backend mapping; this table coordinates
//! in-process scope delegation while a Worker is running.
mod conflict;
mod error;
mod lifecycle;
mod mutate;
mod table;
#[cfg(test)]
mod test_util;
pub use conflict::{
ConflictOwner, find_conflict_owner, find_conflict_owners, is_within_effective_write,
};
pub use error::ScopeLockError;
pub use lifecycle::{
ScopeAllocationGuard, SegmentLockInfo, adopt_allocation, install_top_level,
install_top_level_with_deny, lookup_segment, update_segment,
};
pub use mutate::{
delegate_scope, reclaim_delegated_scope, reclaim_stale, reclaim_stale_with, register_worker,
register_worker_with_deny, release_worker,
};
pub use table::{Allocation, LockFile, LockFileGuard, default_allocation_path};

View File

@ -6,7 +6,7 @@
use manifest::{Permission, ScopeRule};
use crate::table::{Allocation, LockFile};
use super::table::{Allocation, LockFile};
/// Whether `a` and `b` claim any overlapping concrete path.
///
@ -156,9 +156,11 @@ fn find_conflict_in_subtree(
#[cfg(test)]
mod tests {
use super::super::test_util::*;
use super::super::{
ScopeLockError, delegate_scope, register_worker, register_worker_with_deny,
};
use super::*;
use crate::test_util::*;
use crate::{ScopeLockError, delegate_scope, register_pod, register_worker_with_deny};
use tempfile::TempDir;
#[test]

View File

@ -1,4 +1,4 @@
//! Error type for mutating pod-registry operations.
//! Error type for mutating Worker allocation operations.
use std::io;
use std::path::PathBuf;
@ -6,14 +6,14 @@ use std::path::PathBuf;
use manifest::{ScopeError, ScopeRule};
use session_store::SegmentId;
/// Errors raised by the mutating pod-registry operations.
/// Errors raised by the mutating Worker allocation operations.
#[derive(Debug, thiserror::Error)]
pub enum ScopeLockError {
#[error("I/O error on workers.json: {0}")]
Io(#[from] io::Error),
#[error("pod name `{0}` is already registered")]
#[error("worker `{0}` is already allocated")]
DuplicateWorkerName(String),
#[error("requested scope `{}` conflicts with pod `{competitor}` rule `{}`", .rule.target.display(), .competitor_rule.target.display())]
#[error("requested scope `{}` conflicts with worker allocation `{competitor}` rule `{}`", .rule.target.display(), .competitor_rule.target.display())]
WriteConflict {
competitor: String,
rule: ScopeRule,
@ -26,10 +26,10 @@ pub enum ScopeLockError {
NotSubset { spawner: String, rule: ScopeRule },
#[error("invalid delegation scope: {source}")]
InvalidScope { source: ScopeError },
#[error("pod `{0}` is not registered")]
#[error("worker `{0}` is not allocated")]
UnknownWorker(String),
#[error(
"session {segment_id} is already held by pod `{worker_name}` at {}",
"session {segment_id} is already allocated to worker `{worker_name}` at {}",
.socket.display()
)]
SegmentConflict {

View File

@ -1,5 +1,5 @@
//! Owned-allocation guards and the high-level entry points that open
//! the default registry path, mutate it, and return a guard that cleans
//! the default worker allocation path, mutate it, and return a guard that cleans
//! up on drop.
use std::path::{Path, PathBuf};
@ -7,9 +7,9 @@ use std::path::{Path, PathBuf};
use manifest::ScopeRule;
use session_store::SegmentId;
use crate::error::ScopeLockError;
use crate::mutate::release_worker;
use crate::table::{LockFileGuard, default_registry_path};
use super::error::ScopeLockError;
use super::mutate::release_worker;
use super::table::{LockFileGuard, default_allocation_path};
/// Owned allocation: on drop, opens the lock file and releases this
/// Worker's entry. The guard keeps only the name + lock-file path; it
@ -68,9 +68,9 @@ pub fn install_top_level_with_deny(
scope_deny: Vec<ScopeRule>,
segment_id: SegmentId,
) -> Result<ScopeAllocationGuard, ScopeLockError> {
let lock_path = default_registry_path()?;
let lock_path = default_allocation_path()?;
let mut guard = LockFileGuard::open(&lock_path)?;
crate::mutate::register_worker_with_deny(
super::mutate::register_worker_with_deny(
&mut guard,
worker_name.clone(),
pid,
@ -99,7 +99,7 @@ pub fn adopt_allocation(
new_pid: u32,
segment_id: SegmentId,
) -> Result<ScopeAllocationGuard, ScopeLockError> {
let lock_path = default_registry_path()?;
let lock_path = default_allocation_path()?;
let mut guard = LockFileGuard::open(&lock_path)?;
let alloc = guard
.data_mut()
@ -134,7 +134,7 @@ pub fn adopt_allocation(
/// guard, so the segment_id collision check is atomic with the
/// rewrite.
pub fn update_segment(worker_name: &str, new_segment_id: SegmentId) -> Result<(), ScopeLockError> {
let lock_path = default_registry_path()?;
let lock_path = default_allocation_path()?;
let mut guard = LockFileGuard::open(&lock_path)?;
if let Some(other) = guard.data().find_by_segment(new_segment_id) {
if other.worker_name != worker_name {
@ -169,9 +169,9 @@ pub struct SegmentLockInfo {
/// Used by `Worker::restore_from_manifest` to refuse a resume that would
/// race a live writer on the same source session.
pub fn lookup_segment(segment_id: SegmentId) -> Result<Option<SegmentLockInfo>, ScopeLockError> {
let lock_path = default_registry_path()?;
let lock_path = default_allocation_path()?;
let mut guard = LockFileGuard::open(&lock_path)?;
crate::mutate::reclaim_stale(&mut guard);
super::mutate::reclaim_stale(&mut guard);
Ok(guard
.data()
.find_by_segment(segment_id)
@ -184,9 +184,9 @@ pub fn lookup_segment(segment_id: SegmentId) -> Result<Option<SegmentLockInfo>,
#[cfg(test)]
mod tests {
use super::super::table::Allocation;
use super::super::test_util::*;
use super::*;
use crate::table::Allocation;
use crate::test_util::*;
use tempfile::TempDir;
/// Mimic what the spawner does before the child comes up: push an

View File

@ -1,5 +1,5 @@
//! Mutating operations over the allocation table. All of these expect
//! the caller to hold a [`LockFileGuard`] for the registry's lock file.
//! the caller to hold a [`LockFileGuard`] for the worker allocation's lock file.
use std::io;
use std::path::PathBuf;
@ -7,9 +7,9 @@ use std::path::PathBuf;
use manifest::{DelegationScope, Permission, ScopeRule};
use session_store::SegmentId;
use crate::conflict::{find_conflict_owner, find_conflict_owners};
use crate::error::ScopeLockError;
use crate::table::{Allocation, LockFileGuard};
use super::conflict::{find_conflict_owner, find_conflict_owners};
use super::error::ScopeLockError;
use super::table::{Allocation, LockFileGuard};
/// Register a top-level Worker (started directly by a human, no
/// delegation parent). Reclaims stale entries before checking
@ -46,7 +46,7 @@ pub fn register_worker(
/// competitor.rule), not relational — it does not verify that the
/// competitor actually descends from this Worker's prior delegations.
/// In practice this is safe because the canonical restore caller derives
/// `scope_deny` from outstanding `pod-store` child delegations, so any
/// `scope_deny` from outstanding child worker metadata delegations, so any
/// covered competitor is expected to be a descendant of the original
/// allocation. Direct callers must uphold the same invariant.
pub fn register_worker_with_deny(
@ -79,7 +79,7 @@ pub fn register_worker_with_deny(
scope_deny
.iter()
.filter(|r| r.permission == Permission::Write)
.any(|deny| crate::conflict::covers_fully(deny, &owner.rule))
.any(|deny| super::conflict::covers_fully(deny, &owner.rule))
});
if all_denied {
continue;
@ -286,9 +286,9 @@ fn pid_alive(pid: u32) -> bool {
#[cfg(test)]
mod tests {
use super::super::is_within_effective_write;
use super::super::test_util::*;
use super::*;
use crate::is_within_effective_write;
use crate::test_util::*;
use tempfile::TempDir;
#[test]

View File

@ -49,7 +49,7 @@ pub struct Allocation {
/// a top-level Worker started directly by a human.
pub delegated_from: Option<String>,
/// Segment ID this Worker is currently writing to. `None` means this
/// is a pre-reservation made by a spawner via [`crate::delegate_scope`]
/// is a pre-reservation made by a spawner via [`super::super::delegate_scope`]
/// before the child has come up; the child fills it in at
/// [`crate::adopt_allocation`] time.
#[serde(default)]
@ -79,11 +79,11 @@ impl LockFile {
}
/// Default on-disk path: `<runtime_dir>/workers.json` resolved via
/// [`manifest::paths::pod_registry_path`]. Tests should point this
/// [`manifest::paths::worker_allocation_path`]. Tests should point this
/// elsewhere by setting `YOI_HOME` or `YOI_RUNTIME_DIR` to a
/// tempdir.
pub fn default_registry_path() -> io::Result<PathBuf> {
paths::pod_registry_path().ok_or_else(|| {
pub fn default_allocation_path() -> io::Result<PathBuf> {
paths::worker_allocation_path().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"could not resolve workers.json path (no YOI_HOME / \
@ -137,7 +137,7 @@ impl LockFileGuard {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!(
"timed out waiting for pod registry lock `{}`",
"timed out waiting for worker allocation lock `{}`",
path.display()
),
));
@ -149,7 +149,7 @@ impl LockFileGuard {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
format!(
"timed out waiting for pod registry lock `{}`",
"timed out waiting for worker allocation lock `{}`",
path.display()
),
));
@ -211,9 +211,9 @@ impl Drop for LockFileGuard {
#[cfg(test)]
mod tests {
use super::super::register_worker;
use super::super::test_util::*;
use super::*;
use crate::register_pod;
use crate::test_util::*;
use tempfile::TempDir;
#[test]
@ -277,7 +277,7 @@ mod tests {
sid(),
)
.unwrap();
crate::delegate_scope(
super::super::delegate_scope(
&mut g,
"parent",
"child".into(),

View File

@ -1,6 +1,6 @@
//! Shared test helpers for the pod-registry crate.
//! Shared test helpers for the pod-worker allocation crate.
//!
//! Visible to all `#[cfg(test)]` modules under `crate::test_util::*`.
//! Visible to all `#[cfg(test)]` modules under `super::test_util::*`.
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex, MutexGuard};
@ -8,7 +8,7 @@ use std::sync::{LazyLock, Mutex, MutexGuard};
use manifest::{DelegationScope, Permission, ScopeConfig, ScopeRule};
use session_store::SegmentId;
use crate::table::LockFileGuard;
use super::table::LockFileGuard;
pub(crate) fn sid() -> SegmentId {
session_store::new_segment_id()
@ -17,7 +17,7 @@ pub(crate) fn sid() -> SegmentId {
/// Serialises tests that mutate runtime-dir env vars. The test
/// harness runs tests on multiple threads inside a single process,
/// so env-var writes from one test would otherwise leak into a
/// parallel test's `default_registry_path()` lookup.
/// parallel test's `default_allocation_path()` lookup.
pub(crate) static ENV_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
/// Sandbox `YOI_RUNTIME_DIR` to a tempdir for the duration of

View File

@ -16,9 +16,9 @@ use std::time::Duration;
use async_trait::async_trait;
use manifest::paths;
use pod_store::{CombinedStore, FsWorkerStore};
use protocol::{Method, Segment, WorkerStatus};
use session_store::FsStore;
use session_store::{CombinedStore, FsWorkerStore};
use tokio::runtime::Runtime;
use tokio::sync::broadcast;
use worker_runtime::execution::{
@ -49,7 +49,7 @@ pub struct ProfileRuntimeWorkerFactory {
workspace_root: PathBuf,
cwd: PathBuf,
store_dir: Option<PathBuf>,
pod_store_dir: Option<PathBuf>,
worker_metadata_dir: Option<PathBuf>,
profile: Option<String>,
runtime_base_dir: Option<PathBuf>,
}
@ -61,7 +61,7 @@ impl ProfileRuntimeWorkerFactory {
cwd: workspace_root.clone(),
workspace_root,
store_dir: None,
pod_store_dir: None,
worker_metadata_dir: None,
profile: None,
runtime_base_dir: None,
}
@ -77,8 +77,8 @@ impl ProfileRuntimeWorkerFactory {
self
}
pub fn with_pod_store_dir(mut self, pod_store_dir: impl Into<PathBuf>) -> Self {
self.pod_store_dir = Some(pod_store_dir.into());
pub fn with_worker_metadata_dir(mut self, worker_metadata_dir: impl Into<PathBuf>) -> Self {
self.worker_metadata_dir = Some(worker_metadata_dir.into());
self
}
@ -104,11 +104,11 @@ impl ProfileRuntimeWorkerFactory {
})
}
fn pod_store_dir(&self, store_dir: &std::path::Path) -> PathBuf {
self.pod_store_dir
fn worker_metadata_dir(&self, store_dir: &std::path::Path) -> PathBuf {
self.worker_metadata_dir
.clone()
.or_else(|| paths::data_dir().map(|data_dir| data_dir.join("pods")))
.or_else(|| store_dir.parent().map(|parent| parent.join("pods")))
.or_else(|| paths::data_dir().map(|data_dir| data_dir.join("workers")))
.or_else(|| store_dir.parent().map(|parent| parent.join("workers")))
.unwrap_or_else(|| PathBuf::from("workers"))
}
@ -174,14 +174,14 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
store_dir.display()
)
})?;
let pod_store_dir = self.pod_store_dir(&store_dir);
let pod_store = FsWorkerStore::new(&pod_store_dir).map_err(|err| {
let worker_metadata_dir = self.worker_metadata_dir(&store_dir);
let worker_metadata_store = FsWorkerStore::new(&worker_metadata_dir).map_err(|err| {
format!(
"failed to initialize worker metadata store at {}: {err}",
pod_store_dir.display()
worker_metadata_dir.display()
)
})?;
let store = CombinedStore::new(session_store, pod_store);
let store = CombinedStore::new(session_store, worker_metadata_store);
let worker = Worker::from_manifest_with_context(
manifest,
@ -558,7 +558,7 @@ mod tests {
runtime_base: PathBuf,
cwd: PathBuf,
store_dir: PathBuf,
pod_store_dir: PathBuf,
worker_metadata_dir: PathBuf,
}
#[async_trait]
@ -588,7 +588,7 @@ mod tests {
.map_err(|err| err.to_string())?;
let store = CombinedStore::new(
FsStore::new(&self.store_dir).map_err(|err| err.to_string())?,
FsWorkerStore::new(&self.pod_store_dir).map_err(|err| err.to_string())?,
FsWorkerStore::new(&self.worker_metadata_dir).map_err(|err| err.to_string())?,
);
let scope = Scope::writable(&self.cwd).map_err(|err| err.to_string())?;
let worker = Worker::new(
@ -662,7 +662,7 @@ mod tests {
runtime_base: runtime_base.path().to_path_buf(),
cwd: cwd.path().to_path_buf(),
store_dir: store.path().join("sessions"),
pod_store_dir: store.path().join("pods"),
worker_metadata_dir: store.path().join("workers"),
};
let backend = WorkerRuntimeExecutionBackend::new(factory).unwrap();
let runtime = EmbeddedRuntime::with_execution_backend(

View File

@ -21,7 +21,7 @@ use std::sync::Arc;
use std::time::Duration;
use manifest::{Permission, ScopeRule, SharedScope};
use pod_store::{
use session_store::{
WorkerMetadataStore, WorkerReclaimedChild, WorkerSpawnedChild, WorkerSpawnedScopeRule,
WorkerStoreError,
};
@ -30,7 +30,7 @@ use tokio::sync::Mutex;
use tracing::warn;
use crate::runtime::dir::{RuntimeDir, SpawnedWorkerRecord};
use crate::runtime::pod_registry;
use crate::runtime::worker_allocation;
type RegistryStateWriter = Arc<dyn Fn(&[SpawnedWorkerRecord]) -> io::Result<()> + Send + Sync>;
type RegistryReclaimWriter = Arc<dyn Fn(&SpawnedWorkerRecord) -> io::Result<()> + Send + Sync>;
@ -339,11 +339,11 @@ fn reclaim_record(
.cloned()
.collect::<Vec<_>>();
let lock_path = pod_registry::default_registry_path()
let lock_path = worker_allocation::default_allocation_path()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let mut guard = pod_registry::LockFileGuard::open(&lock_path)
let mut guard = worker_allocation::LockFileGuard::open(&lock_path)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
pod_registry::reclaim_delegated_scope(
worker_allocation::reclaim_delegated_scope(
&mut guard,
parent_name,
&record.worker_name,
@ -361,12 +361,12 @@ fn reclaim_record(
}
fn release_child_allocation(worker_name: &str) -> io::Result<()> {
let lock_path = pod_registry::default_registry_path()
let lock_path = worker_allocation::default_allocation_path()
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
let mut guard = pod_registry::LockFileGuard::open(&lock_path)
let mut guard = worker_allocation::LockFileGuard::open(&lock_path)
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
match pod_registry::release_worker(&mut guard, worker_name) {
Ok(()) | Err(pod_registry::ScopeLockError::UnknownWorker(_)) => Ok(()),
match worker_allocation::release_worker(&mut guard, worker_name) {
Ok(()) | Err(worker_allocation::ScopeLockError::UnknownWorker(_)) => Ok(()),
Err(err) => Err(io::Error::new(io::ErrorKind::Other, err)),
}
}

View File

@ -1,9 +1,9 @@
//! `SpawnWorker` tool — launch a new Worker process as a child of this one.
//!
//! Wires pod-registry delegation, child manifest-config construction, subprocess
//! Wires worker-allocation delegation, child manifest-config construction, subprocess
//! launch, and socket handoff into a single `Tool` implementation. When
//! the LLM calls `SpawnWorker`, a fresh Worker runtime command is exec'd in its own
//! process group, the pod-registry is updated atomically, and the child's
//! process group, the worker-allocation is updated atomically, and the child's
//! first turn is kicked off by handing its socket a `Method::Run`.
use std::path::{Path, PathBuf};
@ -29,7 +29,7 @@ use tokio::time::sleep;
use crate::ipc::event;
use crate::prompt::catalog::PromptCatalog;
use crate::runtime::dir::SpawnedWorkerRecord;
use crate::runtime::pod_registry::{self, LockFileGuard, ScopeLockError};
use crate::runtime::worker_allocation::{self, LockFileGuard, ScopeLockError};
use crate::spawn::comm_tools::{SendRunError, send_run_and_confirm};
use crate::spawn::registry::SpawnedWorkerRegistry;
use protocol::WorkerEvent;
@ -216,7 +216,7 @@ fn parse_spawn_profile_selector(raw: Option<&str>) -> Result<SpawnProfileSelecto
/// controller once per Worker lifetime.
pub struct SpawnWorkerTool {
/// Spawner's own worker name — becomes the spawned Worker's
/// `delegated_from` in the pod-registry.
/// `delegated_from` in the worker-allocation.
spawner_name: String,
/// Path to the spawner's Unix socket. Handed to the child via
/// `--callback` so its `WorkerEvent` callbacks have somewhere to land.
@ -254,7 +254,7 @@ pub struct SpawnWorkerTool {
/// `Permission::Write` rules in the delegated scope are revoked
/// from the spawner's in-memory view (a `deny(Write, target)` is
/// pushed on top, downgrading the spawner's effective access on
/// those paths to `Read`). Mirrors the pod-registry's
/// those paths to `Read`). Mirrors the worker-allocation's
/// `effective_write` semantics: Write is the only permission
/// tracked across Workers, so revocation only touches Write.
spawner_scope: SharedScope,
@ -337,15 +337,15 @@ impl Tool for SpawnWorkerTool {
.map_err(|e| ToolError::InvalidArgument(format!("{e}")))?;
let predicted_socket = self.runtime_base.join(&input.name).join("sock");
let lock_path = pod_registry::default_registry_path()
.map_err(|e| ToolError::ExecutionFailed(format!("pod-registry path: {e}")))?;
let lock_path = worker_allocation::default_allocation_path()
.map_err(|e| ToolError::ExecutionFailed(format!("worker-allocation path: {e}")))?;
// Reserve the allocation up front. Spawner's pid is a live
// placeholder; the child will rewrite it via `adopt_allocation`.
{
let mut guard = LockFileGuard::open(&lock_path)
.map_err(|e| ToolError::ExecutionFailed(format!("pod-registry open: {e}")))?;
pod_registry::delegate_scope(
.map_err(|e| ToolError::ExecutionFailed(format!("worker-allocation open: {e}")))?;
worker_allocation::delegate_scope(
&mut guard,
&self.spawner_name,
input.name.clone(),
@ -354,7 +354,7 @@ impl Tool for SpawnWorkerTool {
scope_allow.clone(),
&self.delegation_scope,
)
.map_err(pod_registry_err_to_tool)?;
.map_err(worker_allocation_err_to_tool)?;
}
// `start_outcome` covers steps that happen before the child is
@ -527,7 +527,7 @@ impl SpawnWorkerTool {
fn release_reservation(&self, lock_path: &Path, worker_name: &str) {
if let Ok(mut g) = LockFileGuard::open(lock_path) {
let _ = pod_registry::release_worker(&mut g, worker_name);
let _ = worker_allocation::release_worker(&mut g, worker_name);
}
}
}
@ -864,7 +864,7 @@ fn spawn_delivery_error(worker_name: &str, err: SendRunError) -> ToolError {
}
}
fn pod_registry_err_to_tool(e: ScopeLockError) -> ToolError {
fn worker_allocation_err_to_tool(e: ScopeLockError) -> ToolError {
match e {
ScopeLockError::NotSubset { .. }
| ScopeLockError::WriteConflict { .. }

View File

@ -10,7 +10,7 @@ use tracing::{debug, warn};
use crate::discovery::{WeakNotifyDelivery, WorkerDiscovery};
use crate::hook::{Hook, HookPostToolAction, PostToolCall, ToolResultSummary};
use crate::prompt::catalog::{PromptCatalog, WorkerPrompt};
use pod_store::WorkerMetadataStore;
use session_store::WorkerMetadataStore;
const MAX_TITLE_CHARS: usize = 96;
const MAX_SUMMARY_CHARS: usize = 160;
@ -251,11 +251,11 @@ mod tests {
use crate::runtime::dir::RuntimeDir;
use crate::spawn::registry::SpawnedWorkerRegistry;
use llm_engine::tool::ToolOutput;
use pod_store::FsWorkerStore;
use pod_store::WorkerMetadata;
use protocol::stream::{JsonLineReader, JsonLineWriter};
use protocol::{Event, Method};
use serde_json::json;
use session_store::FsWorkerStore;
use session_store::WorkerMetadata;
use std::sync::Arc;
use tempfile::tempdir;
use ticket::NewTicket;

View File

@ -10,13 +10,13 @@ use llm_engine::llm_client::client::LlmClient;
use llm_engine::llm_client::types::Role;
use llm_engine::state::Mutable;
use llm_engine::{Engine, EngineError, EngineResult, ToolOutputLimits, UsageRecord};
use pod_store::{
WorkerActiveSegmentRef, WorkerMetadata, WorkerMetadataStore, WorkerReclaimedChild,
WorkerSpawnedChild, WorkerSpawnedScopeRule, WorkerStoreError,
};
use session_store::{
LogEntry, SegmentId, SessionId, Store, StoreError, SystemItem, segment_log, to_logged,
};
use session_store::{
WorkerActiveSegmentRef, WorkerMetadata, WorkerMetadataStore, WorkerReclaimedChild,
WorkerSpawnedChild, WorkerSpawnedScopeRule, WorkerStoreError,
};
use tracing::{info, warn};
use crate::segment_log_sink::SegmentLogSink;
@ -44,7 +44,7 @@ use crate::prompt::catalog::{CatalogError, PromptCatalog};
use crate::prompt::loader::PromptLoader;
use crate::prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
use crate::runtime::dir;
use crate::runtime::pod_registry::{self, ScopeAllocationGuard, ScopeLockError};
use crate::runtime::worker_allocation::{self, ScopeAllocationGuard, ScopeLockError};
use crate::workflow::WorkflowResolveError;
#[cfg(test)]
use async_trait::async_trait;
@ -795,7 +795,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
/// Strip `revoke` rules from the Worker's runtime scope by adding
/// matching deny rules. A `Permission::Write` revoke caps effective
/// access at `Read` (mirroring the pod-registry `effective_write`
/// access at `Read` (mirroring the worker-allocation `effective_write`
/// semantics — Write is the only permission tracked across Workers).
/// A `Permission::Read` revoke removes access entirely.
pub fn revoke_scope_rules(
@ -2086,7 +2086,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
self.segment_state.set_entries_written(1);
self.sink.reset_with_initial(entry);
if self.scope_allocation.is_some() {
pod_registry::update_segment(&self.manifest.worker.name, fork_segment_id)?;
worker_allocation::update_segment(&self.manifest.worker.name, fork_segment_id)?;
}
self.write_worker_metadata_active(SegmentLocation {
session_id: loc.session_id,
@ -2795,7 +2795,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
// when no allocation is installed (e.g. compact under
// `Worker::new` in tests).
if self.scope_allocation.is_some() {
pod_registry::update_segment(&self.manifest.worker.name, new_segment_id)?;
worker_allocation::update_segment(&self.manifest.worker.name, new_segment_id)?;
}
self.write_worker_metadata_active(SegmentLocation {
session_id: old_loc.session_id,
@ -3847,19 +3847,19 @@ where
// Segment creation is deferred to the first run (see
// `ensure_segment_head`) so the SegmentStart entry can capture
// the rendered system prompt, not the raw template source. The
// session_id + segment_id are allocated here so the pod-registry
// session_id + segment_id are allocated here so the worker-allocation
// registration can record them from the start.
let session_id = session_store::new_session_id();
let segment_id = session_store::new_segment_id();
// Register this Worker in the machine-wide pod-registry
// Register this Worker in the machine-wide worker-allocation
// before building anything else, so a spawn that conflicts on
// scope fails fast.
let socket_path = dir::default_base()
.map_err(ScopeLockError::from)?
.join(&manifest.worker.name)
.join("sock");
let scope_allocation = pod_registry::install_top_level(
let scope_allocation = worker_allocation::install_top_level(
manifest.worker.name.clone(),
std::process::id(),
socket_path,
@ -3927,7 +3927,7 @@ where
///
/// Behaves like [`Worker::from_manifest`] but claims the scope
/// allocation that the spawner pre-registered via
/// [`pod_registry::delegate_scope`], rather than installing a new
/// [`worker_allocation::delegate_scope`], rather than installing a new
/// top-level entry. `callback_socket` carries the spawner's
/// Unix-socket path so the spawned Worker can send `Method::Notify`
/// back to the spawner.
@ -3971,7 +3971,7 @@ where
// fresh Session rather than joining the spawner's.
let session_id = session_store::new_session_id();
let segment_id = session_store::new_segment_id();
let scope_allocation = pod_registry::adopt_allocation(
let scope_allocation = worker_allocation::adopt_allocation(
manifest.worker.name.clone(),
std::process::id(),
segment_id,
@ -4104,9 +4104,9 @@ where
/// reuses the same `segment_id` so subsequent turns append to the
/// source jsonl as a continuation of the same conversation.
///
/// Concurrent writers are prevented by the pod-registry:
/// Concurrent writers are prevented by the worker-allocation:
/// the registration carries `segment_id`, and this constructor
/// refuses to start when `pod_registry::lookup_segment` already finds
/// refuses to start when `worker_allocation::lookup_segment` already finds
/// a live Worker writing to `segment_id`. So there is no need to fork —
/// resume is "the same session, a different process owning it".
///
@ -4173,7 +4173,7 @@ where
.map_err(ScopeLockError::from)?
.join(&manifest.worker.name)
.join("sock");
let scope_allocation = pod_registry::install_top_level_with_deny(
let scope_allocation = worker_allocation::install_top_level_with_deny(
manifest.worker.name.clone(),
std::process::id(),
socket_path,
@ -4291,10 +4291,10 @@ where
let delegated_scope = spawned_child_scope_rules(&child);
if !delegated_scope.is_empty() {
let lock_path =
pod_registry::default_registry_path().map_err(ScopeLockError::from)?;
let mut guard =
pod_registry::LockFileGuard::open(&lock_path).map_err(ScopeLockError::from)?;
pod_registry::reclaim_delegated_scope(
worker_allocation::default_allocation_path().map_err(ScopeLockError::from)?;
let mut guard = worker_allocation::LockFileGuard::open(&lock_path)
.map_err(ScopeLockError::from)?;
worker_allocation::reclaim_delegated_scope(
&mut guard,
&worker_name,
&child.worker_name,
@ -5300,7 +5300,7 @@ mod worker_metadata_restore_manifest_tests {
#[test]
fn metadata_writer_persists_workspace_root_through_store_update() {
let temp = tempfile::tempdir().unwrap();
let store = pod_store::FsWorkerStore::new(temp.path().join("pods")).unwrap();
let store = session_store::FsWorkerStore::new(temp.path().join("workers")).unwrap();
let workspace_root = temp.path().join("workspace-root");
std::fs::create_dir_all(&workspace_root).unwrap();
let writer = worker_metadata_writer_for_store(&store);

View File

@ -16,8 +16,8 @@ use llm_engine::Engine;
use llm_engine::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent};
use llm_engine::llm_client::types::Item;
use llm_engine::llm_client::{ClientError, LlmClient, Request};
use pod_store::{CombinedStore, FsWorkerStore, WorkerMetadataStore};
use protocol::{Event, Method, RunResult};
use session_store::{CombinedStore, FsWorkerStore, WorkerMetadataStore};
use session_store::{FsStore, LogEntry, Store};
use tokio::sync::broadcast;

View File

@ -26,8 +26,8 @@ use llm_engine::llm_client::{ClientError, LlmClient, Request};
use memory::WorkspaceLayout;
use memory::extract::{ExtractedPayload, write_staging};
use memory::schema::SourceRef;
use pod_store::{CombinedStore, FsWorkerStore};
use session_store::FsStore;
use session_store::{CombinedStore, FsWorkerStore};
type TestStore = CombinedStore<FsStore, FsWorkerStore>;
use tokio::sync::broadcast;

View File

@ -9,7 +9,7 @@ use llm_engine::llm_client::event::{ErrorEvent, Event as LlmEvent, ResponseStatu
use llm_engine::llm_client::types::Item;
use llm_engine::llm_client::{ClientError, LlmClient, Request};
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use pod_store::{CombinedStore, FsWorkerStore};
use session_store::{CombinedStore, FsWorkerStore};
use session_store::{FsStore, LogEntry};
use worker::{Event, Method, Worker, WorkerController, WorkerHandle, WorkerManifest, WorkerStatus};

View File

@ -2,12 +2,12 @@
//! validation paths.
//!
//! These cases all return before `prepare_worker_common` runs, so they
//! do not need a real LLM client or pod-registry environment — only the
//! do not need a real LLM client or worker-allocation environment — only the
//! session store needs to be present.
use std::sync::{LazyLock, Mutex};
use pod_store::{
use session_store::{
CombinedStore, FsWorkerStore, WorkerActiveSegmentRef, WorkerMetadata, WorkerMetadataStore,
};
use session_store::{FsStore, StoreError};

View File

@ -25,8 +25,8 @@ use llm_engine::Engine;
use llm_engine::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent, UsageEvent};
use llm_engine::llm_client::{ClientError, LlmClient, Request};
use llm_engine::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use pod_store::{CombinedStore, FsWorkerStore};
use session_metrics::{DOMAIN, Metric, metrics_from_extensions};
use session_store::{CombinedStore, FsWorkerStore};
use session_store::{FsStore, LogEntry, SegmentId, SessionId, Store, StoreError, TraceEntry};
use worker::{Worker, WorkerManifest};

View File

@ -1,6 +1,6 @@
//! Integration tests for the `SpawnWorker` tool.
//!
//! These tests exercise the tool's pod-registry delegation, subprocess
//! These tests exercise the tool's worker-allocation delegation, subprocess
//! launch, socket handoff, and `spawned_workers.json` write through an injected
//! typed runtime command. The mock command exits immediately while a
//! test-owned Unix listener pre-binds the predicted socket path, so the tool
@ -22,7 +22,7 @@ use std::sync::Arc;
use tempfile::TempDir;
use tokio::net::UnixListener;
use worker::runtime::dir::{RuntimeDir, SpawnedWorkerRecord};
use worker::runtime::pod_registry::{self, LockFileGuard};
use worker::runtime::worker_allocation::{self, LockFileGuard};
use worker::spawn::registry::SpawnedWorkerRegistry;
use worker::spawn::tool::spawn_worker_tool_with_runtime_command;
@ -67,7 +67,7 @@ async fn setup_spawner(
.unwrap();
let spawner_socket = spawner_rd.socket_path();
let _guard = pod_registry::install_top_level(
let _guard = worker_allocation::install_top_level(
spawner_name.into(),
std::process::id(),
spawner_socket.clone(),
@ -450,8 +450,8 @@ async fn spawn_worker_delegates_scope_and_sends_run() {
other => panic!("expected Run, got {other:?}"),
}
// Verify pod_registry has the child allocation under `root`.
let lock_path = pod_registry::default_registry_path().unwrap();
// Verify worker_allocation has the child allocation under `root`.
let lock_path = worker_allocation::default_allocation_path().unwrap();
let guard = LockFileGuard::open(&lock_path).unwrap();
let child = guard
.data()
@ -651,7 +651,7 @@ async fn spawn_worker_rejects_scope_outside_spawner() {
}
// The spawner's allocation is unchanged; no "child" appeared.
let lock_path = pod_registry::default_registry_path().unwrap();
let lock_path = worker_allocation::default_allocation_path().unwrap();
let guard = LockFileGuard::open(&lock_path).unwrap();
assert!(guard.data().find("child").is_none());
@ -724,7 +724,7 @@ async fn spawn_worker_rolls_back_reservation_when_socket_never_appears() {
}
// Rollback assertion: the reserved "ghost" allocation is gone.
let lock_path = pod_registry::default_registry_path().unwrap();
let lock_path = worker_allocation::default_allocation_path().unwrap();
let guard = LockFileGuard::open(&lock_path).unwrap();
assert!(
guard.data().find("ghost").is_none(),

View File

@ -8,7 +8,7 @@ use futures::Stream;
use llm_engine::Engine;
use llm_engine::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent};
use llm_engine::llm_client::{ClientError, LlmClient, Request};
use pod_store::{CombinedStore, FsWorkerStore};
use session_store::{CombinedStore, FsWorkerStore};
use session_store::{FsStore, LogEntry, Store};
use worker::{PromptLoader, SystemPromptTemplate, Worker, WorkerError};

View File

@ -14,17 +14,17 @@ use std::sync::{Arc, LazyLock, Mutex};
use llm_engine::llm_client::types::{ContentPart, Item, Role};
use llm_engine::tool::ToolOutput;
use manifest::{Permission, Scope, ScopeRule, SharedScope};
use pod_store::{CombinedStore, FsWorkerStore, WorkerMetadataStore};
use protocol::stream::{JsonLineReader, JsonLineWriter};
use protocol::{ErrorCode, Event, Greeting, Method};
use serde_json::json;
use session_store::FsStore;
use session_store::{CombinedStore, FsWorkerStore, WorkerMetadataStore};
use tempfile::TempDir;
use tokio::net::UnixListener;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use worker::runtime::dir::{RuntimeDir, SpawnedWorkerRecord};
use worker::runtime::pod_registry::{self, LockFileGuard};
use worker::runtime::worker_allocation::{self, LockFileGuard};
use worker::spawn::comm_tools::{read_worker_output_tool, send_to_worker_tool, stop_worker_tool};
use worker::spawn::registry::SpawnedWorkerRegistry;
@ -416,7 +416,7 @@ async fn stop_worker_sends_shutdown_and_releases_scope() {
permission: Permission::Write,
recursive: true,
};
pod_registry::register_worker_with_deny(
worker_allocation::register_worker_with_deny(
&mut g,
"spawner".into(),
std::process::id(),
@ -426,7 +426,7 @@ async fn stop_worker_sends_shutdown_and_releases_scope() {
session_store::new_segment_id(),
)
.unwrap();
pod_registry::register_worker(
worker_allocation::register_worker(
&mut g,
"child".into(),
std::process::id(),
@ -663,7 +663,7 @@ async fn load_from_worker_state_reclaims_missing_child_scope_and_records_history
{
let mut g = LockFileGuard::open(&runtime_tmp.path().join("workers.json")).unwrap();
pod_registry::register_worker_with_deny(
worker_allocation::register_worker_with_deny(
&mut g,
"spawner".into(),
std::process::id(),

View File

@ -15,7 +15,7 @@ use tempfile::TempDir;
use tokio::net::UnixListener;
use worker::ipc::event::{apply_event_side_effects, fire_and_forget, render_event};
use worker::runtime::dir::{RuntimeDir, SpawnedWorkerRecord};
use worker::runtime::pod_registry::{self, LockFileGuard};
use worker::runtime::worker_allocation::{self, LockFileGuard};
use worker::spawn::registry::SpawnedWorkerRegistry;
/// Serialises tests that mutate `YOI_RUNTIME_DIR`.
@ -62,7 +62,7 @@ impl Drop for EnvGuard {
}
}
/// Point `YOI_RUNTIME_DIR` at `dir`. The pod-registry then lives at
/// Point `YOI_RUNTIME_DIR` at `dir`. The worker-allocation then lives at
/// `<dir>/workers.json` and Worker runtime sub-dirs at `<dir>/{worker_name}/`.
fn set_runtime_dir(dir: &std::path::Path) {
unsafe {
@ -380,7 +380,7 @@ async fn shutdown_releases_scope_allocation_when_present() {
// Install a top-level allocation for "kid" so ShutDown has
// something to release.
let guard = pod_registry::install_top_level(
let guard = worker_allocation::install_top_level(
"kid".into(),
std::process::id(),
"/tmp/kid.sock".into(),
@ -412,7 +412,7 @@ async fn shutdown_releases_scope_allocation_when_present() {
)
.await;
// Allocation is gone from the pod-registry.
// Allocation is gone from the worker-allocation.
let g = LockFileGuard::open(&lock_path).unwrap();
assert!(
g.data().find("kid").is_none(),

View File

@ -10,6 +10,7 @@ async-trait.workspace = true
axum = { workspace = true, features = ["ws"] }
chrono = { version = "0.4", default-features = false, features = ["clock"] }
futures.workspace = true
manifest.workspace = true
protocol = { workspace = true }
project-record.workspace = true
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "native-tls"] }
@ -23,7 +24,7 @@ ticket.workspace = true
tokio = { workspace = true, features = ["fs", "macros", "net", "rt-multi-thread", "sync"] }
tokio-tungstenite.workspace = true
worker = { workspace = true, features = ["runtime-adapter"] }
worker-runtime = { workspace = true, features = ["ws-server"] }
worker-runtime = { workspace = true, features = ["ws-server", "fs-store"] }
toml.workspace = true
tracing.workspace = true
uuid = { workspace = true, features = ["v7"] }

View File

@ -2,7 +2,7 @@ use std::sync::{Arc, Mutex};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use worker_runtime::catalog::{CapabilityRequest, ConfigBundleRef, ProfileSelector};
use worker_runtime::catalog::ProfileSelector;
use worker_runtime::config_bundle::{
ConfigBundle, ConfigBundleMetadata, ConfigBundleProvenance, ConfigProfileDescriptor,
};
@ -367,10 +367,6 @@ fn spawn_companion_worker(runtime: &RuntimeRegistry) -> CompanionWorkerState {
let selector = companion_profile_selector();
let mut diagnostics = Vec::new();
let config_bundle = companion_config_bundle();
let config_ref = ConfigBundleRef {
id: config_bundle.metadata.id.clone(),
digest: config_bundle.metadata.digest.clone(),
};
match runtime.sync_config_bundle(COMPANION_RUNTIME_ID, config_bundle) {
Ok(result) => diagnostics.extend(result.diagnostics),
@ -390,8 +386,7 @@ fn spawn_companion_worker(runtime: &RuntimeRegistry) -> CompanionWorkerState {
expected_segments: 0,
},
profile: Some(selector),
config_bundle: Some(config_ref),
requested_capabilities: vec![CapabilityRequest::named("worker.input.user")],
initial_input: None,
},
);
@ -611,36 +606,37 @@ mod tests {
}
#[test]
fn companion_spawns_worker_with_companion_profile_and_diagnostic_when_not_input_capable() {
let registry =
RuntimeRegistry::for_workspace(EmbeddedWorkerRuntime::new_memory("local:test"));
fn companion_spawns_worker_with_companion_profile_through_runtime_backend() {
let registry = RuntimeRegistry::for_workspace(
EmbeddedWorkerRuntime::new_memory_with_execution_backend(
"local:test",
Arc::new(DeterministicExecutionBackend::default()),
)
.unwrap(),
);
let registry = Arc::new(registry);
let companion = CompanionConsole::new(registry.clone());
let status = companion.status();
let worker = status.worker.clone().expect("companion worker");
assert_eq!(worker.runtime_id, COMPANION_RUNTIME_ID);
assert_eq!(worker.role.as_deref(), Some("workspace_companion"));
assert!(!worker.capabilities.can_accept_input);
assert_eq!(status.transport.completion, "not_input_capable");
assert!(
status
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "companion_worker_not_input_capable")
);
assert_eq!(worker.role.as_deref(), Some(COMPANION_PROFILE_ID));
assert!(worker.capabilities.can_accept_input);
assert_eq!(status.transport.completion, "connected");
assert!(status.diagnostics.is_empty());
let response = companion.send_message(CompanionMessageRequest {
content: "hello".to_string(),
});
assert_eq!(response.state, CompanionState::Rejected);
assert_eq!(response.state, CompanionState::Accepted);
assert!(response.diagnostics.is_empty());
assert!(
!response
.diagnostics
response
.transcript
.items
.iter()
.any(|diagnostic| diagnostic.code == "companion_llm_not_connected")
.any(|entry| entry.role == "user" && entry.content == "hello")
);
assert!(response.transcript.items.is_empty());
let worker_detail = registry
.worker(COMPANION_RUNTIME_ID, &worker.worker_id)

View File

@ -6,14 +6,20 @@ use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::{sync::Arc, time::Duration};
use std::{path::PathBuf, sync::Arc, time::Duration};
use worker_runtime::catalog::{
CapabilityRequest, ConfigBundleRef, CreateWorkerRequest, ProfileSelector,
WorkerDetail as EmbeddedWorkerDetail, WorkerIntent, WorkerStatus as EmbeddedWorkerStatus,
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, WorkerDetail as EmbeddedWorkerDetail,
WorkerStatus as EmbeddedWorkerStatus,
};
use worker_runtime::config_bundle::{
ConfigBundle, ConfigBundleAvailability, ConfigBundleMetadata, ConfigBundleProvenance,
ConfigBundleSummary, ConfigProfileDescriptor,
};
use worker_runtime::config_bundle::{ConfigBundle, ConfigBundleAvailability, ConfigBundleSummary};
use worker_runtime::error::RuntimeError as EmbeddedRuntimeError;
use worker_runtime::execution::WorkerExecutionRunState;
use worker_runtime::execution::{
WorkerExecutionBackendKind, WorkerExecutionRunState, WorkerExecutionStatus,
};
use worker_runtime::fs_store::FsRuntimeStoreOptions;
use worker_runtime::http_server::{
RuntimeHttpConfigBundleAvailabilityResponse, RuntimeHttpConfigBundleSyncRequest,
RuntimeHttpErrorResponse, RuntimeHttpSummaryResponse, RuntimeHttpTranscriptResponse,
@ -236,11 +242,12 @@ pub struct WorkerLookupResult {
/// Browser-safe worker spawn request shape.
///
/// The request intentionally carries only workspace policy intents, stable
/// worker identifiers, optional profile selectors, config bundle refs, and
/// requested capability names. Raw workspace roots, child cwd, executable path,
/// Runtime endpoints/credentials, raw bundle storage paths, and host-local
/// resolved WorkerSpec content are resolved by the runtime service and never
/// The request carries Browser-facing launch semantics only: workspace intent,
/// optional display identity, acceptance policy, optional profile selector, and
/// optional initial input. Runtime execution authority is resolved by the host
/// into a synced ConfigBundle before the canonical Runtime create request is
/// built. Raw workspace roots, child cwd, executable paths, tool scope,
/// credentials, raw config stores, sockets, sessions, and storage paths are not
/// accepted from Workspace API callers.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WorkerSpawnRequest {
@ -251,9 +258,7 @@ pub struct WorkerSpawnRequest {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<ProfileSelector>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config_bundle: Option<ConfigBundleRef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requested_capabilities: Vec<CapabilityRequest>,
pub initial_input: Option<EmbeddedWorkerInput>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@ -907,29 +912,46 @@ pub struct EmbeddedWorkerRuntime {
execution_enabled: bool,
}
fn embedded_runtime_options() -> EmbeddedRuntimeOptions {
let runtime_id = EmbeddedRuntimeId::new(EMBEDDED_RUNTIME_ID)
.expect("embedded runtime id is a non-empty literal");
EmbeddedRuntimeOptions {
runtime_id: Some(runtime_id),
display_name: Some("Workspace backend embedded Runtime".to_string()),
..EmbeddedRuntimeOptions::default()
}
}
impl EmbeddedWorkerRuntime {
pub fn new_memory(workspace_id: impl AsRef<str>) -> Self {
let runtime_id = EmbeddedRuntimeId::new(EMBEDDED_RUNTIME_ID)
.expect("embedded runtime id is a non-empty literal");
let runtime = worker_runtime::Runtime::with_options(EmbeddedRuntimeOptions {
runtime_id: Some(runtime_id),
display_name: Some("Workspace backend embedded Runtime".to_string()),
..EmbeddedRuntimeOptions::default()
});
let runtime = worker_runtime::Runtime::with_options(embedded_runtime_options());
Self::from_runtime(workspace_id, runtime)
}
pub fn new_memory_with_execution_backend(
workspace_id: impl AsRef<str>,
backend: std::sync::Arc<dyn worker_runtime::execution::WorkerExecutionBackend>,
) -> Result<Self, worker_runtime::error::RuntimeError> {
let runtime =
worker_runtime::Runtime::with_execution_backend(embedded_runtime_options(), backend)?;
let mut embedded = Self::from_runtime(workspace_id, runtime);
embedded.execution_enabled = true;
Ok(embedded)
}
pub fn new_fs_store_with_execution_backend(
workspace_id: impl AsRef<str>,
store_root: impl Into<PathBuf>,
backend: std::sync::Arc<dyn worker_runtime::execution::WorkerExecutionBackend>,
) -> Result<Self, worker_runtime::error::RuntimeError> {
let runtime_id = EmbeddedRuntimeId::new(EMBEDDED_RUNTIME_ID)
.expect("embedded runtime id is a non-empty literal");
let runtime = worker_runtime::Runtime::with_execution_backend(
EmbeddedRuntimeOptions {
let runtime = worker_runtime::Runtime::with_fs_store_and_execution_backend(
FsRuntimeStoreOptions {
root: store_root.into(),
runtime_id: Some(runtime_id),
display_name: Some("Workspace backend embedded Runtime".to_string()),
..EmbeddedRuntimeOptions::default()
limits: EmbeddedRuntimeOptions::default().limits,
},
backend,
)?;
@ -964,11 +986,7 @@ impl EmbeddedWorkerRuntime {
status: EmbeddedWorkerStatus,
execution: &worker_runtime::execution::WorkerExecutionStatus,
) -> bool {
self.execution_enabled
&& status == EmbeddedWorkerStatus::Running
&& execution.backend == worker_runtime::execution::WorkerExecutionBackendKind::Connected
&& execution.run_state == WorkerExecutionRunState::Idle
&& !execution_last_result_blocks_control(execution)
runtime_worker_can_accept_input(self.execution_enabled, status, execution)
}
fn can_stop_embedded_worker(
@ -976,16 +994,7 @@ impl EmbeddedWorkerRuntime {
status: EmbeddedWorkerStatus,
execution: &worker_runtime::execution::WorkerExecutionStatus,
) -> bool {
self.execution_enabled
&& status == EmbeddedWorkerStatus::Running
&& execution.backend == worker_runtime::execution::WorkerExecutionBackendKind::Connected
&& !matches!(
execution.run_state,
WorkerExecutionRunState::Rejected
| WorkerExecutionRunState::Errored
| WorkerExecutionRunState::Unconnected
)
&& !execution_last_result_blocks_control(execution)
runtime_worker_can_stop(self.execution_enabled, status, execution)
}
fn map_worker_summary(&self, summary: worker_runtime::catalog::WorkerSummary) -> WorkerSummary {
@ -994,14 +1003,14 @@ impl EmbeddedWorkerRuntime {
worker_id: summary.worker_ref.worker_id.as_str().to_string(),
host_id: self.host_id.clone(),
label: safe_display_hint(summary.worker_ref.worker_id.as_str()),
role: embedded_intent_label(&summary.intent),
role: embedded_profile_label(&summary.profile),
profile: embedded_profile_label(&summary.profile),
workspace: WorkerWorkspaceSummary {
visibility: "backend_internal".to_string(),
identity: "runtime_registry_worker".to_string(),
},
state: embedded_worker_status_label(summary.status).to_string(),
status: embedded_worker_execution_status_label(summary.status, summary.execution.run_state)
status: embedded_worker_execution_status_label(summary.status, &summary.execution)
.to_string(),
last_seen_at: None,
implementation: WorkerImplementationSummary {
@ -1009,15 +1018,12 @@ impl EmbeddedWorkerRuntime {
display_hint: "backend-internal worker-runtime Worker".to_string(),
},
capabilities: WorkerCapabilitySummary {
can_accept_input: self.can_accept_embedded_input(summary.status, &summary.execution),
can_accept_input: self
.can_accept_embedded_input(summary.status, &summary.execution),
can_stop: self.can_stop_embedded_worker(summary.status, &summary.execution),
can_spawn_followup: false,
},
diagnostics: vec![diagnostic(
"embedded_runtime_projection",
DiagnosticSeverity::Info,
"Worker identity is projected only as runtime_id plus worker_id; embedded runtime internals remain backend-private".to_string(),
)],
diagnostics: embedded_worker_projection_diagnostics(&summary.execution),
}
}
@ -1027,14 +1033,14 @@ impl EmbeddedWorkerRuntime {
worker_id: detail.worker_id.as_str().to_string(),
host_id: self.host_id.clone(),
label: safe_display_hint(detail.worker_id.as_str()),
role: embedded_intent_label(&detail.intent),
role: embedded_profile_label(&detail.profile),
profile: embedded_profile_label(&detail.profile),
workspace: WorkerWorkspaceSummary {
visibility: "backend_internal".to_string(),
identity: "runtime_registry_worker".to_string(),
},
state: embedded_worker_status_label(detail.status).to_string(),
status: embedded_worker_execution_status_label(detail.status, detail.execution.run_state)
status: embedded_worker_execution_status_label(detail.status, &detail.execution)
.to_string(),
last_seen_at: None,
implementation: WorkerImplementationSummary {
@ -1046,11 +1052,7 @@ impl EmbeddedWorkerRuntime {
can_stop: self.can_stop_embedded_worker(detail.status, &detail.execution),
can_spawn_followup: false,
},
diagnostics: vec![diagnostic(
"embedded_runtime_projection",
DiagnosticSeverity::Info,
"Worker identity is projected only as runtime_id plus worker_id; embedded runtime internals remain backend-private".to_string(),
)],
diagnostics: embedded_worker_projection_diagnostics(&detail.execution),
}
}
}
@ -1195,26 +1197,35 @@ impl WorkspaceWorkerRuntime for EmbeddedWorkerRuntime {
if matches!(request.acceptance, WorkerSpawnAcceptanceRequirement::RunAccepted { expected_segments } if expected_segments > 0)
{
diagnostics.push(diagnostic(
"embedded_runtime_tools_less",
"embedded_runtime_acceptance_projection",
DiagnosticSeverity::Info,
"Embedded Runtime v0 creates a tools-less catalog Worker and does not spawn provider segments".to_string(),
"Embedded Runtime accepts creation through a runtime execution backend; provider segment counts are observed after execution, not faked at create time".to_string(),
));
}
let profile = request
.profile
.clone()
.unwrap_or_else(|| embedded_profile_selector(&request.intent));
let config_bundle = match self
.runtime
.store_config_bundle(default_embedded_config_bundle(&profile))
{
Ok(availability) => availability.reference,
Err(error) => {
diagnostics.push(embedded_runtime_diagnostic(&error));
return WorkerSpawnResult {
state: WorkerOperationState::Rejected,
worker: None,
acceptance_evidence: Vec::new(),
diagnostics,
};
}
};
let create_request = CreateWorkerRequest {
intent: embedded_create_intent(&request.intent),
profile: request
.profile
.clone()
.unwrap_or_else(|| embedded_profile_selector(&request.intent)),
config_bundle: request.config_bundle.clone(),
requested_capabilities: if request.requested_capabilities.is_empty() {
vec![CapabilityRequest::named("read")]
} else {
request.requested_capabilities.clone()
},
workspace_refs: Vec::new(),
mount_refs: Vec::new(),
profile,
config_bundle,
initial_input: request.initial_input.clone(),
};
match self.runtime.create_worker(create_request) {
Ok(detail) => {
@ -1675,22 +1686,23 @@ impl RemoteWorkerRuntime {
worker_id: summary.worker_ref.worker_id.as_str().to_string(),
host_id: self.host_id.clone(),
label: safe_display_hint(summary.worker_ref.worker_id.as_str()),
role: embedded_intent_label(&summary.intent),
role: None,
profile: embedded_profile_label(&summary.profile),
workspace: WorkerWorkspaceSummary {
visibility: "remote_runtime".to_string(),
identity: "runtime_registry_worker".to_string(),
},
state: embedded_worker_status_label(summary.status).to_string(),
status: embedded_worker_status_label(summary.status).to_string(),
status: embedded_worker_execution_status_label(summary.status, &summary.execution)
.to_string(),
last_seen_at: None,
implementation: WorkerImplementationSummary {
kind: "remote_worker_runtime".to_string(),
display_hint: "Backend-proxied remote worker-runtime Worker".to_string(),
},
capabilities: WorkerCapabilitySummary {
can_accept_input: true,
can_stop: true,
can_accept_input: runtime_worker_can_accept_input(true, summary.status, &summary.execution),
can_stop: runtime_worker_can_stop(true, summary.status, &summary.execution),
can_spawn_followup: false,
},
diagnostics: vec![diagnostic(
@ -1707,22 +1719,23 @@ impl RemoteWorkerRuntime {
worker_id: detail.worker_id.as_str().to_string(),
host_id: self.host_id.clone(),
label: safe_display_hint(detail.worker_id.as_str()),
role: embedded_intent_label(&detail.intent),
role: None,
profile: embedded_profile_label(&detail.profile),
workspace: WorkerWorkspaceSummary {
visibility: "remote_runtime".to_string(),
identity: "runtime_registry_worker".to_string(),
},
state: embedded_worker_status_label(detail.status).to_string(),
status: embedded_worker_status_label(detail.status).to_string(),
status: embedded_worker_execution_status_label(detail.status, &detail.execution)
.to_string(),
last_seen_at: None,
implementation: WorkerImplementationSummary {
kind: "remote_worker_runtime".to_string(),
display_hint: "Backend-proxied remote worker-runtime Worker".to_string(),
},
capabilities: WorkerCapabilitySummary {
can_accept_input: true,
can_stop: true,
can_accept_input: runtime_worker_can_accept_input(true, detail.status, &detail.execution),
can_stop: runtime_worker_can_stop(true, detail.status, &detail.execution),
can_spawn_followup: false,
},
diagnostics: vec![diagnostic(
@ -1875,20 +1888,24 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
)],
};
}
let profile = request
.profile
.clone()
.unwrap_or_else(|| embedded_profile_selector(&request.intent));
let sync = self.sync_config_bundle(default_embedded_config_bundle(&profile));
let Some(config_bundle) = sync.availability.map(|availability| availability.reference)
else {
return WorkerSpawnResult {
state: WorkerOperationState::Rejected,
worker: None,
acceptance_evidence: Vec::new(),
diagnostics: sync.diagnostics,
};
};
let create = CreateWorkerRequest {
intent: embedded_create_intent(&request.intent),
profile: request
.profile
.clone()
.unwrap_or_else(|| embedded_profile_selector(&request.intent)),
config_bundle: request.config_bundle.clone(),
requested_capabilities: if request.requested_capabilities.is_empty() {
vec![CapabilityRequest::named("read")]
} else {
request.requested_capabilities.clone()
},
workspace_refs: Vec::new(),
mount_refs: Vec::new(),
profile,
config_bundle,
initial_input: request.initial_input.clone(),
};
match self.post_json::<_, RuntimeHttpWorkerResponse>("/v1/workers", &create) {
Ok(response) => WorkerSpawnResult {
@ -2099,9 +2116,37 @@ fn embedded_spawn_execution_failure_diagnostic(
))
}
fn execution_last_result_blocks_control(
execution: &worker_runtime::execution::WorkerExecutionStatus,
fn runtime_worker_can_accept_input(
execution_enabled: bool,
status: EmbeddedWorkerStatus,
execution: &WorkerExecutionStatus,
) -> bool {
execution_enabled
&& status == EmbeddedWorkerStatus::Running
&& execution.backend == worker_runtime::execution::WorkerExecutionBackendKind::Connected
&& execution.run_state == WorkerExecutionRunState::Idle
&& !execution_last_result_blocks_control(execution)
}
fn runtime_worker_can_stop(
execution_enabled: bool,
status: EmbeddedWorkerStatus,
execution: &WorkerExecutionStatus,
) -> bool {
execution_enabled
&& status == EmbeddedWorkerStatus::Running
&& execution.backend == worker_runtime::execution::WorkerExecutionBackendKind::Connected
&& !matches!(
execution.run_state,
WorkerExecutionRunState::Stopped
| WorkerExecutionRunState::Rejected
| WorkerExecutionRunState::Errored
| WorkerExecutionRunState::Unconnected
)
&& !execution_last_result_blocks_control(execution)
}
fn execution_last_result_blocks_control(execution: &WorkerExecutionStatus) -> bool {
execution.last_result.as_ref().is_some_and(|result| {
matches!(
result.outcome,
@ -2120,39 +2165,90 @@ fn embedded_worker_status_label(status: EmbeddedWorkerStatus) -> &'static str {
}
}
fn embedded_worker_projection_diagnostics(
execution: &WorkerExecutionStatus,
) -> Vec<RuntimeDiagnostic> {
let mut diagnostics = vec![diagnostic(
"embedded_runtime_projection",
DiagnosticSeverity::Info,
"Worker identity is projected only as runtime_id plus worker_id; embedded runtime internals remain backend-private".to_string(),
)];
if execution.backend == WorkerExecutionBackendKind::Stale {
diagnostics.push(diagnostic(
"embedded_worker_execution_stale",
DiagnosticSeverity::Warning,
"Worker execution handle is not connected in this server process; persisted execution binding was marked stale".to_string(),
));
} else if execution.backend == WorkerExecutionBackendKind::Unconnected
|| execution.run_state == WorkerExecutionRunState::Unconnected
{
diagnostics.push(diagnostic(
"embedded_worker_execution_unconnected",
DiagnosticSeverity::Warning,
"Worker execution handle is not connected in this server process".to_string(),
));
} else if execution.run_state == WorkerExecutionRunState::Rejected {
diagnostics.push(diagnostic(
"embedded_worker_execution_spawn_rejected",
DiagnosticSeverity::Error,
"Worker execution spawn was rejected; backend-private details are not exposed"
.to_string(),
));
}
diagnostics
}
fn embedded_worker_execution_status_label(
status: EmbeddedWorkerStatus,
run_state: WorkerExecutionRunState,
execution: &WorkerExecutionStatus,
) -> &'static str {
match status {
EmbeddedWorkerStatus::Stopped => "stopped",
EmbeddedWorkerStatus::Cancelled => "cancelled",
EmbeddedWorkerStatus::Running => match run_state {
WorkerExecutionRunState::Idle => "idle",
WorkerExecutionRunState::Busy => "running",
WorkerExecutionRunState::Stopped => "stopped",
WorkerExecutionRunState::Rejected => "rejected",
WorkerExecutionRunState::Errored => "errored",
WorkerExecutionRunState::Unconnected => "unconnected",
},
EmbeddedWorkerStatus::Running => {
if execution.backend == worker_runtime::execution::WorkerExecutionBackendKind::Stale {
return "stale";
}
match execution.run_state {
WorkerExecutionRunState::Idle => "idle",
WorkerExecutionRunState::Busy => "running",
WorkerExecutionRunState::Stopped => "stopped",
WorkerExecutionRunState::Rejected => "rejected",
WorkerExecutionRunState::Errored => "errored",
WorkerExecutionRunState::Unconnected => "unconnected",
}
}
}
}
fn embedded_create_intent(intent: &WorkerSpawnIntent) -> WorkerIntent {
match intent {
WorkerSpawnIntent::WorkspaceCompanion => WorkerIntent::Role {
role: "workspace_companion".to_string(),
purpose: Some("workspace backend internal companion".to_string()),
},
WorkerSpawnIntent::WorkspaceOrchestrator => WorkerIntent::Role {
role: "workspace_orchestrator".to_string(),
purpose: Some("workspace backend internal orchestration".to_string()),
},
WorkerSpawnIntent::TicketRole { ticket_id, role } => WorkerIntent::Role {
role: ticket_role_profile_slug(role).to_string(),
purpose: Some(format!("ticket {ticket_id}")),
fn default_embedded_config_bundle(profile: &ProfileSelector) -> ConfigBundle {
let id = format!(
"workspace-runtime-{}",
embedded_profile_label(profile)
.unwrap_or_else(|| "default".to_string())
.replace([':', '/', ' '], "-")
);
ConfigBundle {
metadata: ConfigBundleMetadata {
id,
digest: String::new(),
revision: "workspace-runtime-v0".to_string(),
workspace_id: "workspace-server".to_string(),
created_at: "runtime-generated".to_string(),
provenance: ConfigBundleProvenance {
source: "workspace-server".to_string(),
detail: Some("backend-resolved launch bundle".to_string()),
},
},
profiles: vec![ConfigProfileDescriptor {
selector: profile.clone(),
label: embedded_profile_label(profile),
}],
declarations: Vec::new(),
}
.with_computed_digest()
}
fn embedded_profile_selector(intent: &WorkerSpawnIntent) -> ProfileSelector {
@ -2176,16 +2272,6 @@ fn ticket_role_profile_slug(role: &TicketWorkerRole) -> &'static str {
}
}
fn embedded_intent_label(intent: &WorkerIntent) -> Option<String> {
match intent {
WorkerIntent::Assistant { purpose } => {
purpose.clone().or_else(|| Some("assistant".to_string()))
}
WorkerIntent::Task { objective } => Some(safe_display_hint(objective)),
WorkerIntent::Role { role, .. } => Some(safe_display_hint(role)),
}
}
fn embedded_profile_label(profile: &ProfileSelector) -> Option<String> {
Some(match profile {
ProfileSelector::RuntimeDefault => "runtime_default".to_string(),
@ -2324,7 +2410,8 @@ fn embedded_runtime_diagnostic(error: &EmbeddedRuntimeError) -> RuntimeDiagnosti
DiagnosticSeverity::Warning,
"Embedded Runtime worker was not found".to_string(),
),
EmbeddedRuntimeError::WorkerExecutionUnavailable { .. } => diagnostic(
EmbeddedRuntimeError::WorkerExecutionUnavailable { .. }
| EmbeddedRuntimeError::ExecutionBackendUnavailable { .. } => diagnostic(
"embedded_worker_execution_unavailable",
DiagnosticSeverity::Warning,
"Embedded Worker has no execution backend attached".to_string(),
@ -2339,6 +2426,11 @@ fn embedded_runtime_diagnostic(error: &EmbeddedRuntimeError) -> RuntimeDiagnosti
DiagnosticSeverity::Warning,
format!("Requested limit {requested} exceeds embedded Runtime maximum {max}"),
),
EmbeddedRuntimeError::InvalidInitialInputKind { .. } => diagnostic(
"embedded_worker_initial_input_kind_invalid",
DiagnosticSeverity::Warning,
error.to_string(),
),
EmbeddedRuntimeError::InvalidRequest(_)
| EmbeddedRuntimeError::ConfigBundleMissing { .. }
| EmbeddedRuntimeError::ConfigBundleDigestMismatch { .. }
@ -2962,8 +3054,7 @@ mod tests {
expected_segments: 0,
},
profile: None,
config_bundle: None,
requested_capabilities: Vec::new(),
initial_input: None,
}
}
@ -2978,13 +3069,35 @@ mod tests {
assert_eq!(spawned.state, WorkerOperationState::Rejected);
assert!(spawned.acceptance_evidence.is_empty());
assert!(spawned.diagnostics.iter().any(|diagnostic| {
diagnostic.code == "embedded_worker_execution_spawn_errored"
diagnostic.code == "embedded_worker_execution_rejected"
&& !diagnostic.message.contains("/tmp/secret-provider-config")
}));
let worker = spawned.worker.expect("failed execution is still projected");
assert_eq!(worker.status, "errored");
assert!(!worker.capabilities.can_accept_input);
assert!(!worker.capabilities.can_stop);
assert!(spawned.worker.is_none());
}
#[test]
fn embedded_runtime_rejects_system_initial_input_without_worker_projection() {
let runtime = EmbeddedWorkerRuntime::new_memory_with_execution_backend(
"local:test",
Arc::new(AcceptingExecutionBackend::default()),
)
.expect("test backend should connect");
let mut request = embedded_spawn_request();
request.initial_input = Some(EmbeddedWorkerInput {
kind: EmbeddedWorkerInputKind::System,
content: "system/role instruction belongs in profile".to_string(),
});
let spawned = runtime.spawn_worker(request);
assert_eq!(spawned.state, WorkerOperationState::Rejected);
assert!(spawned.worker.is_none());
assert!(spawned.diagnostics.iter().any(|diagnostic| {
diagnostic.code == "embedded_worker_initial_input_kind_invalid"
&& diagnostic
.message
.contains("initial worker input must be user input")
}));
assert!(runtime.list_workers(10).items.is_empty());
}
#[test]
@ -3035,8 +3148,13 @@ mod tests {
#[test]
fn embedded_runtime_registers_routes_input_and_transcript_without_internal_leaks() {
let registry =
RuntimeRegistry::for_workspace(EmbeddedWorkerRuntime::new_memory("local:test"));
let registry = RuntimeRegistry::for_workspace(
EmbeddedWorkerRuntime::new_memory_with_execution_backend(
"local:test",
Arc::new(AcceptingExecutionBackend::default()),
)
.expect("test backend should connect"),
);
let runtimes = registry.list_runtimes(10);
let embedded_summary = runtimes
@ -3050,7 +3168,7 @@ mod tests {
);
assert_eq!(embedded_summary.source.status, RuntimeSourceStatus::Active);
assert!(embedded_summary.capabilities.can_spawn_worker);
assert!(!embedded_summary.capabilities.can_accept_input);
assert!(embedded_summary.capabilities.can_accept_input);
let spawned = registry
.spawn_worker(
@ -3065,8 +3183,7 @@ mod tests {
expected_segments: 0,
},
profile: None,
config_bundle: None,
requested_capabilities: Vec::new(),
initial_input: None,
},
)
.unwrap();
@ -3083,7 +3200,7 @@ mod tests {
assert_eq!(worker.workspace.identity, "runtime_registry_worker");
assert_eq!(worker.implementation.kind, "embedded_worker_runtime");
assert_eq!(worker.profile.as_deref(), Some("builtin:coder"));
assert!(!worker.capabilities.can_accept_input);
assert!(worker.capabilities.can_accept_input);
let input = registry
.send_input(
@ -3095,21 +3212,20 @@ mod tests {
},
)
.unwrap();
assert_eq!(input.state, WorkerOperationState::Rejected);
assert_eq!(input.state, WorkerOperationState::Accepted);
assert_eq!(input.runtime_id, EMBEDDED_RUNTIME_ID);
assert_eq!(input.worker_id, worker.worker_id);
assert!(
input
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "embedded_worker_execution_unavailable")
);
let transcript = registry
.transcript(EMBEDDED_RUNTIME_ID, &worker.worker_id, 0, 10)
.unwrap();
assert_eq!(transcript.state, WorkerOperationState::Accepted);
assert!(transcript.items.is_empty());
assert!(
transcript
.items
.iter()
.any(|entry| entry.role == "user" && entry.content == "hello embedded runtime")
);
let json = serde_json::to_string(&(embedded_summary, worker, transcript)).unwrap();
for forbidden in [
@ -3132,9 +3248,13 @@ mod tests {
#[test]
fn embedded_backend_syncs_config_bundle_and_spawns_with_bundle_ref() {
let registry = RuntimeRegistry::new(vec![Arc::new(EmbeddedWorkerRuntime::new_memory(
"local:test",
))]);
let registry = RuntimeRegistry::new(vec![Arc::new(
EmbeddedWorkerRuntime::new_memory_with_execution_backend(
"local:test",
Arc::new(AcceptingExecutionBackend::default()),
)
.unwrap(),
)]);
let bundle = test_config_bundle();
let sync = registry
.sync_config_bundle(EMBEDDED_RUNTIME_ID, bundle.clone())
@ -3162,8 +3282,7 @@ mod tests {
expected_segments: 0,
},
profile: Some(ProfileSelector::Builtin("builtin:coder".to_string())),
config_bundle: Some(reference),
requested_capabilities: vec![CapabilityRequest::named("read")],
initial_input: None,
},
)
.unwrap();
@ -3176,9 +3295,13 @@ mod tests {
#[test]
fn embedded_runtime_rejects_socket_ready_acceptance_without_socket_identity() {
let registry = RuntimeRegistry::new(vec![Arc::new(EmbeddedWorkerRuntime::new_memory(
"local:test",
))]);
let registry = RuntimeRegistry::new(vec![Arc::new(
EmbeddedWorkerRuntime::new_memory_with_execution_backend(
"local:test",
Arc::new(AcceptingExecutionBackend::default()),
)
.unwrap(),
)]);
let result = registry
.spawn_worker(
EMBEDDED_RUNTIME_ID,
@ -3187,8 +3310,7 @@ mod tests {
requested_worker_name: None,
acceptance: WorkerSpawnAcceptanceRequirement::SocketReady,
profile: None,
config_bundle: None,
requested_capabilities: Vec::new(),
initial_input: None,
},
)
.unwrap();
@ -3275,6 +3397,8 @@ mod tests {
workers.items[0].workspace.identity,
"runtime_registry_worker"
);
assert!(workers.items[0].capabilities.can_accept_input);
assert!(workers.items[0].capabilities.can_stop);
let input = registry
.send_input(
@ -3304,6 +3428,101 @@ mod tests {
assert!(browser_payload.contains("worker_id"));
}
#[test]
fn remote_runtime_projection_blocks_stale_and_unconnected_execution_input() {
let (base_url, server) = serve_mock_http(vec![
mock_response(
"GET",
"/v1/workers",
true,
200,
json!({
"workers": [
worker_json_with_execution(
"embedded-worker-runtime",
"worker-stale",
"stale",
"unconnected",
None,
),
worker_json_with_execution(
"embedded-worker-runtime",
"worker-unconnected",
"unconnected",
"unconnected",
None,
),
worker_json_with_execution(
"embedded-worker-runtime",
"worker-rejected",
"connected",
"rejected",
Some("rejected"),
),
worker_json_with_execution(
"embedded-worker-runtime",
"worker-errored",
"connected",
"errored",
Some("errored"),
)
]
})
.to_string(),
),
mock_response(
"GET",
"/v1/workers/worker-stale",
true,
200,
json!({
"worker": worker_json_with_execution(
"embedded-worker-runtime",
"worker-stale",
"stale",
"unconnected",
None,
)})
.to_string(),
),
]);
let registry = RuntimeRegistry::new(vec![Arc::new(
RemoteWorkerRuntime::new(RemoteRuntimeConfig::new(
"remote:primary",
"Remote Primary",
base_url,
Some("secret-token-do-not-leak".to_string()),
))
.unwrap(),
)]);
let workers = registry.list_workers(10);
assert_eq!(workers.items.len(), 4);
for worker in &workers.items {
assert!(
!worker.capabilities.can_accept_input,
"{} should not be input-capable",
worker.worker_id
);
assert!(
!worker.capabilities.can_stop,
"{} should not be stoppable",
worker.worker_id
);
}
assert_eq!(workers.items[0].status, "stale");
assert_eq!(workers.items[1].status, "unconnected");
assert_eq!(workers.items[2].status, "rejected");
assert_eq!(workers.items[3].status, "errored");
let stale_detail = registry.worker("remote:primary", "worker-stale").unwrap();
assert!(!stale_detail.capabilities.can_accept_input);
assert!(!stale_detail.capabilities.can_stop);
assert_eq!(stale_detail.status, "stale");
server.join().expect("mock remote server finished");
}
#[test]
fn remote_config_bundle_sync_and_check_diagnostics_are_sanitized_and_path_safe() {
let leaked_store_path = "/var/lib/yoi/runtime/bundles/bundle-1.json";
@ -3472,20 +3691,33 @@ mod tests {
}
fn worker_json(runtime_id: &str, worker_id: &str) -> serde_json::Value {
worker_json_with_execution(runtime_id, worker_id, "connected", "idle", None)
}
fn worker_json_with_execution(
runtime_id: &str,
worker_id: &str,
backend: &str,
run_state: &str,
last_outcome: Option<&str>,
) -> serde_json::Value {
let last_result = last_outcome.map(|outcome| {
json!({
"operation": "input",
"outcome": outcome,
"run_state": run_state,
"message": format!("{outcome} result")
})
});
json!({
"worker_ref": { "runtime_id": runtime_id, "worker_id": worker_id },
"runtime_id": runtime_id,
"worker_id": worker_id,
"status": "running",
"execution": { "backend": "connected", "run_state": "idle" },
"execution": { "backend": backend, "run_state": run_state, "last_result": last_result },
"intent": { "kind": "role", "role": "coder", "purpose": "remote test" },
"profile": { "kind": "builtin", "value": "coder" },
"config_bundle": null,
"requested_capabilities": [],
"workspace_refs": [],
"mount_refs": [],
"requested_capability_count": 0,
"has_config_bundle": false,
"config_bundle": { "id": "remote-bundle", "digest": "remote-digest" },
"transcript_len": 0,
"last_event_id": 0
})

View File

@ -51,6 +51,7 @@ pub struct ServerConfig {
pub workspace_display_name: String,
pub workspace_created_at: String,
pub workspace_root: PathBuf,
pub embedded_runtime_store_root: PathBuf,
pub static_assets_dir: Option<PathBuf>,
pub auth: AuthConfig,
pub max_records: usize,
@ -61,11 +62,14 @@ pub struct ServerConfig {
impl ServerConfig {
pub fn local_dev(workspace_root: impl Into<PathBuf>, identity: WorkspaceIdentity) -> Self {
let workspace_root = workspace_root.into();
let workspace_id = identity.workspace_id;
let embedded_runtime_store_root = Self::default_embedded_runtime_store_root(&workspace_id);
Self {
workspace_id: identity.workspace_id,
workspace_id,
workspace_display_name: identity.display_name,
workspace_created_at: identity.created_at,
workspace_root,
embedded_runtime_store_root,
static_assets_dir: None,
auth: AuthConfig::LocalDevToken {
token_configured: false,
@ -75,6 +79,35 @@ impl ServerConfig {
remote_runtime_sources: Vec::new(),
}
}
pub fn embedded_runtime_store_root_for_data_dir(
data_dir: impl Into<PathBuf>,
workspace_id: impl AsRef<str>,
) -> PathBuf {
data_dir
.into()
.join("workspace-server")
.join(workspace_id.as_ref())
.join("embedded-runtime")
}
pub fn default_embedded_runtime_store_root(workspace_id: impl AsRef<str>) -> PathBuf {
match manifest::paths::data_dir() {
Some(data_dir) => {
Self::embedded_runtime_store_root_for_data_dir(data_dir, workspace_id.as_ref())
}
None => std::env::temp_dir()
.join("yoi")
.join("workspace-server")
.join(workspace_id.as_ref())
.join("embedded-runtime"),
}
}
pub fn with_embedded_runtime_store_root(mut self, root: impl Into<PathBuf>) -> Self {
self.embedded_runtime_store_root = root.into();
self
}
}
#[derive(Clone)]
@ -115,8 +148,9 @@ impl WorkspaceApi {
})
.await?;
let mut runtime = RuntimeRegistry::for_workspace(
EmbeddedWorkerRuntime::new_memory_with_execution_backend(
EmbeddedWorkerRuntime::new_fs_store_with_execution_backend(
config.workspace_id.clone(),
config.embedded_runtime_store_root.clone(),
execution_backend,
)
.map_err(|err| {
@ -1063,10 +1097,15 @@ mod tests {
use axum::http::Request;
use futures::{SinkExt, StreamExt};
use serde_json::{Value, json};
use std::sync::Arc;
use tokio_tungstenite::connect_async;
use tokio_tungstenite::tungstenite::Message;
use tower::ServiceExt;
use crate::hosts::{
TicketWorkerRole, WorkerInputKind, WorkerOperationState, WorkerSpawnAcceptanceRequirement,
WorkerSpawnIntent,
};
use crate::observation::ClientWorkerEventWsDiagnostic;
use crate::store::SqliteWorkspaceStore;
@ -1140,6 +1179,58 @@ mod tests {
}
}
fn test_server_config(workspace_root: impl Into<PathBuf>) -> ServerConfig {
let workspace_root = workspace_root.into();
let store_root = workspace_root.join(".test-embedded-runtime-store");
ServerConfig::local_dev(workspace_root, test_identity())
.with_embedded_runtime_store_root(store_root)
}
fn runtime_test_bundle() -> worker_runtime::config_bundle::ConfigBundle {
worker_runtime::config_bundle::ConfigBundle {
metadata: worker_runtime::config_bundle::ConfigBundleMetadata {
id: "server-test-bundle".to_string(),
digest: String::new(),
revision: "test".to_string(),
workspace_id: "test".to_string(),
created_at: "test".to_string(),
provenance: worker_runtime::config_bundle::ConfigBundleProvenance {
source: "test".to_string(),
detail: None,
},
},
profiles: vec![worker_runtime::config_bundle::ConfigProfileDescriptor {
selector: worker_runtime::catalog::ProfileSelector::RuntimeDefault,
label: Some("server-test".to_string()),
}],
declarations: Vec::new(),
}
.with_computed_digest()
}
fn runtime_create_request() -> worker_runtime::catalog::CreateWorkerRequest {
let bundle = runtime_test_bundle();
worker_runtime::catalog::CreateWorkerRequest {
profile: worker_runtime::catalog::ProfileSelector::RuntimeDefault,
config_bundle: worker_runtime::catalog::ConfigBundleRef {
id: bundle.metadata.id,
digest: bundle.metadata.digest,
},
initial_input: None,
}
}
fn runtime_with_worker() -> (worker_runtime::Runtime, worker_runtime::identity::WorkerRef) {
let runtime = worker_runtime::Runtime::with_execution_backend(
worker_runtime::RuntimeOptions::default(),
Arc::new(DeterministicExecutionBackend::default()),
)
.unwrap();
runtime.store_config_bundle(runtime_test_bundle()).unwrap();
let worker = runtime.create_worker(runtime_create_request()).unwrap();
(runtime, worker.worker_ref)
}
#[tokio::test]
async fn serves_bounded_read_apis_and_static_spa_separately() {
let dir = tempfile::tempdir().unwrap();
@ -1151,9 +1242,15 @@ mod tests {
std::fs::write(static_dir.join("assets/app.js"), "console.log('yoi');").unwrap();
let store = SqliteWorkspaceStore::in_memory().unwrap();
let mut config = ServerConfig::local_dev(dir.path(), test_identity());
let mut config = test_server_config(dir.path());
config.static_assets_dir = Some(static_dir);
let api = WorkspaceApi::new(config, Arc::new(store)).await.unwrap();
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(store),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let app = build_router(api);
let workspace = get_json(app.clone(), "/api/workspace").await;
@ -1260,7 +1357,7 @@ mod tests {
let worker_items = workers["items"].as_array().unwrap();
let companion_worker = worker_items
.iter()
.find(|worker| worker["role"] == "workspace_companion")
.find(|worker| worker["role"] == "builtin:companion")
.expect("companion worker is visible through runtime worker API");
assert_eq!(companion_worker["runtime_id"], "embedded-worker-runtime");
assert!(companion_worker["capabilities"]["can_stop"].is_boolean());
@ -1270,7 +1367,7 @@ mod tests {
companion_status["state"].as_str(),
Some("ready") | Some("error")
));
assert_eq!(companion_status["worker"]["role"], "workspace_companion");
assert_eq!(companion_status["worker"]["role"], "builtin:companion");
assert_eq!(
companion_status["transport"]["kind"],
"embedded_worker_runtime"
@ -1305,7 +1402,7 @@ mod tests {
.as_array()
.unwrap()
.iter()
.any(|worker| worker["role"] == "workspace_companion")
.any(|worker| worker["role"] == "builtin:companion")
);
let runs_response = app
@ -1389,7 +1486,7 @@ mod tests {
#[tokio::test]
async fn legacy_companion_messages_route_dispatches_through_worker_runtime() {
let temp = tempfile::tempdir().unwrap();
let config = ServerConfig::local_dev(temp.path().join("workspace"), test_identity());
let config = test_server_config(temp.path().join("workspace"));
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(SqliteWorkspaceStore::in_memory().unwrap()),
@ -1472,12 +1569,202 @@ mod tests {
);
}
#[tokio::test]
async fn embedded_runtime_fs_store_restores_catalog_config_bundle_transcript_and_stale_execution()
{
let dir = tempfile::tempdir().unwrap();
let config = test_server_config(dir.path().join("workspace"));
let store_root = config.embedded_runtime_store_root.clone();
let bundle = runtime_test_bundle();
let bundle_id = bundle.metadata.id.clone();
let api = WorkspaceApi::new_with_execution_backend(
config.clone(),
Arc::new(SqliteWorkspaceStore::in_memory().unwrap()),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.expect("fs-backed api starts");
let synced = api
.runtime
.sync_config_bundle("embedded-worker-runtime", bundle)
.expect("sync config bundle");
assert_eq!(synced.state, WorkerOperationState::Accepted);
assert!(store_root.exists(), "fs-store root should be created");
let spawned = api
.runtime
.spawn_worker(
"embedded-worker-runtime",
WorkerSpawnRequest {
intent: WorkerSpawnIntent::TicketRole {
ticket_id: "00001KVZSGT0Q".to_string(),
role: TicketWorkerRole::Coder,
},
requested_worker_name: None,
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
},
profile: None,
initial_input: None,
},
)
.expect("spawn worker");
assert_eq!(spawned.state, WorkerOperationState::Accepted);
let worker_id = spawned.worker.expect("created worker").worker_id;
let sent = api
.runtime
.send_input(
"embedded-worker-runtime",
&worker_id,
WorkerInputRequest {
kind: WorkerInputKind::User,
content: "persist me".to_string(),
},
)
.expect("send input");
assert_eq!(sent.state, WorkerOperationState::Accepted);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
loop {
let transcript = api
.runtime
.transcript("embedded-worker-runtime", &worker_id, 0, 10)
.expect("transcript");
if transcript.items.iter().any(|item| {
item.role == "assistant" && item.content == "server companion echoed: persist me"
}) {
assert!(
transcript
.items
.iter()
.any(|item| item.role == "user" && item.content == "persist me")
);
break;
}
assert!(
std::time::Instant::now() < deadline,
"timed out waiting for deterministic transcript"
);
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
drop(api);
let restored = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(SqliteWorkspaceStore::in_memory().unwrap()),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.expect("restored fs-backed api starts");
let restored_worker = restored
.runtime
.worker("embedded-worker-runtime", &worker_id)
.expect("restored worker");
assert_eq!(restored_worker.status, "stale");
assert!(!restored_worker.capabilities.can_accept_input);
assert!(!restored_worker.capabilities.can_stop);
assert!(
restored_worker
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code == "embedded_worker_execution_stale")
);
let bundles = restored
.runtime
.list_config_bundles("embedded-worker-runtime")
.expect("config bundle list");
assert!(
bundles
.bundles
.iter()
.any(|summary| summary.id == bundle_id)
);
let restored_transcript = restored
.runtime
.transcript("embedded-worker-runtime", &worker_id, 0, 10)
.expect("restored transcript");
assert!(
restored_transcript
.items
.iter()
.any(|item| item.role == "user" && item.content == "persist me")
);
assert!(restored_transcript.items.iter().any(|item| {
item.role == "assistant" && item.content == "server companion echoed: persist me"
}));
let rejected_input = restored
.runtime
.send_input(
"embedded-worker-runtime",
&worker_id,
WorkerInputRequest {
kind: WorkerInputKind::User,
content: "should not be routed to stale handle".to_string(),
},
)
.expect("stale worker input is projected as an operation result");
assert_eq!(rejected_input.state, WorkerOperationState::Rejected);
}
#[tokio::test]
async fn embedded_runtime_store_root_is_isolated_and_not_exposed_by_browser_api() {
let dir = tempfile::tempdir().unwrap();
let data_dir = dir.path().join("user-data");
let workspace_root = dir.path().join("workspace");
let default_root =
ServerConfig::embedded_runtime_store_root_for_data_dir(&data_dir, TEST_WORKSPACE_ID);
assert_eq!(
default_root,
data_dir
.join("workspace-server")
.join(TEST_WORKSPACE_ID)
.join("embedded-runtime")
);
assert!(!default_root.starts_with(workspace_root.join(".yoi")));
let config = ServerConfig::local_dev(workspace_root, test_identity())
.with_embedded_runtime_store_root(default_root.clone());
let app = build_router(
WorkspaceApi::new_with_execution_backend(
config,
Arc::new(SqliteWorkspaceStore::in_memory().unwrap()),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap(),
);
let raw_store_root = default_root.to_string_lossy().to_string();
for uri in [
"/api/workspace",
"/api/hosts",
"/api/runtimes",
"/api/workers",
] {
let body = get_json(app.clone(), uri).await;
let serialized = serde_json::to_string(&body).unwrap();
assert!(
!serialized.contains(&raw_store_root),
"{uri} leaked embedded runtime store root: {serialized}"
);
}
}
#[tokio::test]
async fn embedded_runtime_api_routes_by_runtime_and_worker_ids_without_leaking_internals() {
let dir = tempfile::tempdir().unwrap();
let store = SqliteWorkspaceStore::in_memory().unwrap();
let config = ServerConfig::local_dev(dir.path(), test_identity());
let api = WorkspaceApi::new(config, Arc::new(store)).await.unwrap();
let config = test_server_config(dir.path());
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(store),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let app = build_router(api);
let runtimes = get_json(app.clone(), "/api/runtimes").await;
@ -1515,20 +1802,14 @@ mod tests {
}),
)
.await;
assert_eq!(spawned["state"], "rejected");
assert!(
spawned["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|diagnostic| {
diagnostic["code"] == "embedded_worker_execution_spawn_errored"
&& !diagnostic["message"]
.as_str()
.unwrap()
.contains("/workspace/demo")
})
);
assert_eq!(spawned["state"], "accepted");
let diagnostics = spawned["diagnostics"].as_array().unwrap();
assert!(diagnostics.iter().all(|diagnostic| {
!diagnostic["message"]
.as_str()
.unwrap_or_default()
.contains("/workspace/demo")
}));
let worker_id = spawned["worker"]["worker_id"].as_str().unwrap().to_string();
assert_eq!(spawned["worker"]["runtime_id"], "embedded-worker-runtime");
assert_eq!(
@ -1557,16 +1838,10 @@ mod tests {
}),
)
.await;
assert_eq!(accepted["state"], "rejected");
assert_eq!(accepted["state"], "accepted");
assert_eq!(accepted["runtime_id"], "embedded-worker-runtime");
assert_eq!(accepted["worker_id"], worker_id);
assert!(
accepted["diagnostics"]
.as_array()
.unwrap()
.iter()
.any(|diagnostic| diagnostic["code"] == "embedded_worker_execution_unavailable")
);
assert!(accepted["diagnostics"].as_array().unwrap().is_empty());
let transcript = get_json(
app.clone(),
@ -1574,7 +1849,9 @@ mod tests {
)
.await;
assert_eq!(transcript["state"], "accepted");
assert!(transcript["items"].as_array().unwrap().is_empty());
assert!(transcript["items"].as_array().unwrap().iter().any(
|item| item["role"] == "user" && item["content"] == "hello from browser-facing api"
));
let wrong_runtime = app
.clone()
@ -1620,10 +1897,7 @@ mod tests {
#[tokio::test]
async fn proxies_worker_observation_ws_with_backend_cursors_and_diagnostics() {
let runtime = worker_runtime::Runtime::new_memory();
let worker = runtime
.create_worker(worker_runtime::catalog::CreateWorkerRequest::default())
.unwrap();
let (runtime, worker_ref) = runtime_with_worker();
let runtime_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let runtime_addr = runtime_listener.local_addr().unwrap();
tokio::spawn({
@ -1637,7 +1911,7 @@ mod tests {
let dir = tempfile::tempdir().unwrap();
let store = SqliteWorkspaceStore::in_memory().unwrap();
let mut config = ServerConfig::local_dev(dir.path(), test_identity());
let mut config = test_server_config(dir.path());
config
.runtime_event_sources
.push(RuntimeObservationSourceConfig {
@ -1645,11 +1919,17 @@ mod tests {
worker_id: "worker-a".into(),
endpoint: format!(
"ws://{runtime_addr}/v1/workers/{}/events/ws",
worker.worker_ref.worker_id
worker_ref.worker_id
),
bearer_token: None,
});
let api = WorkspaceApi::new(config, Arc::new(store)).await.unwrap();
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(store),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let app_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let app_addr = app_listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(app_listener, build_router(api)).await.unwrap() });
@ -1666,7 +1946,7 @@ mod tests {
runtime
.observe_worker_event(
&worker.worker_ref,
&worker_ref,
protocol::Event::TextDelta {
text: "live".into(),
},
@ -1686,7 +1966,7 @@ mod tests {
let _snapshot = next_client_frame(&mut resumed).await;
runtime
.observe_worker_event(
&worker.worker_ref,
&worker_ref,
protocol::Event::TextDone {
text: "done".into(),
},
@ -1822,10 +2102,7 @@ mod tests {
worker_runtime::identity::WorkerRef,
String,
) {
let runtime = worker_runtime::Runtime::new_memory();
let worker = runtime
.create_worker(worker_runtime::catalog::CreateWorkerRequest::default())
.unwrap();
let (runtime, worker_ref) = runtime_with_worker();
let runtime_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let runtime_addr = runtime_listener.local_addr().unwrap();
tokio::spawn({
@ -1838,9 +2115,9 @@ mod tests {
});
let endpoint = format!(
"ws://{runtime_addr}/v1/workers/{}/events/ws",
worker.worker_ref.worker_id
worker_ref.worker_id
);
(runtime, worker.worker_ref, endpoint)
(runtime, worker_ref, endpoint)
}
async fn spawn_workspace_proxy(
@ -1848,11 +2125,17 @@ mod tests {
) -> (String, tempfile::TempDir) {
let dir = tempfile::tempdir().unwrap();
let store = SqliteWorkspaceStore::in_memory().unwrap();
let mut config = ServerConfig::local_dev(dir.path(), test_identity());
let mut config = test_server_config(dir.path());
let runtime_id = source.runtime_id.clone();
let worker_id = source.worker_id.clone();
config.runtime_event_sources.push(source);
let api = WorkspaceApi::new(config, Arc::new(store)).await.unwrap();
let api = WorkspaceApi::new_with_execution_backend(
config,
Arc::new(store),
Arc::new(DeterministicExecutionBackend::default()),
)
.await
.unwrap();
let app_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let app_addr = app_listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(app_listener, build_router(api)).await.unwrap() });

View File

@ -15,7 +15,6 @@ client = { workspace = true }
memory = { workspace = true }
manifest = { workspace = true }
worker = { workspace = true }
pod-store = { workspace = true }
session-store = { workspace = true }
session-analytics = { workspace = true }
ticket = { workspace = true }

View File

@ -4,8 +4,8 @@ use std::path::PathBuf;
use std::time::{Duration, SystemTime};
use manifest::paths;
use pod_store::{FsWorkerStore, WorkerMetadataStore};
use session_store::{FsStore, SessionId, Store};
use session_store::{FsWorkerStore, WorkerMetadataStore};
use crate::worker_cleanup_cli::parse_duration;
@ -203,8 +203,8 @@ pub fn run_prune_with_roots(
));
}
let session_store = FsStore::new(data_dir.join("sessions")).map_err(to_error)?;
let pod_store = FsWorkerStore::new(data_dir.join("pods")).map_err(to_error)?;
let referenced_sessions = referenced_sessions(&pod_store)?;
let worker_metadata_store = FsWorkerStore::new(data_dir.join("workers")).map_err(to_error)?;
let referenced_sessions = referenced_sessions(&worker_metadata_store)?;
let cutoff = options
.older_than
.map(|older_than| {
@ -315,10 +315,12 @@ pub fn run_prune_with_roots(
})
}
fn referenced_sessions(pod_store: &FsWorkerStore) -> Result<BTreeSet<SessionId>, SessionCliError> {
fn referenced_sessions(
worker_metadata_store: &FsWorkerStore,
) -> Result<BTreeSet<SessionId>, SessionCliError> {
let mut sessions = BTreeSet::new();
for name in pod_store.list_names().map_err(to_error)? {
let metadata = pod_store
for name in worker_metadata_store.list_names().map_err(to_error)? {
let metadata = worker_metadata_store
.read_by_name(&name)
.map_err(to_error)?
.ok_or_else(|| {
@ -358,8 +360,8 @@ pub fn help_text() -> &'static str {
#[cfg(test)]
mod tests {
use super::*;
use pod_store::{WorkerActiveSegmentRef, WorkerMetadata};
use session_store::{Store, new_segment_id, new_session_id};
use session_store::{WorkerActiveSegmentRef, WorkerMetadata};
use std::io::Write;
#[test]
@ -441,7 +443,7 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let data_dir = tmp.path().join("data");
let session_store = FsStore::new(data_dir.join("sessions")).unwrap();
let pod_store = FsWorkerStore::new(data_dir.join("pods")).unwrap();
let worker_metadata_store = FsWorkerStore::new(data_dir.join("workers")).unwrap();
let referenced_session = new_session_id();
let referenced_segment = new_segment_id();
let orphan_session = new_session_id();
@ -452,7 +454,7 @@ mod tests {
session_store
.create_segment(orphan_session, orphan_segment, &[])
.unwrap();
pod_store
worker_metadata_store
.write(&WorkerMetadata::new(
"agent",
Some(WorkerActiveSegmentRef::active_segment(

View File

@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use manifest::paths;
use pod_store::{FsWorkerStore, WorkerMetadata, WorkerMetadataStore, validate_worker_name};
use session_store::{FsWorkerStore, WorkerMetadata, WorkerMetadataStore, validate_worker_name};
const MAX_REPORT_ITEMS: usize = 50;
@ -234,12 +234,12 @@ async fn run_delete(
data_dir: PathBuf,
runtime_dir: PathBuf,
) -> Result<WorkerCleanupCliOutput, WorkerCleanupCliError> {
let store = FsWorkerStore::new(data_dir.join("pods")).map_err(to_error)?;
let store = FsWorkerStore::new(data_dir.join("workers")).map_err(to_error)?;
let metadata = store.read_by_name(&options.name).map_err(to_error)?;
let Some(metadata) = metadata else {
return Ok(WorkerCleanupCliOutput {
stdout: format!(
"yoi worker delete\nstatus: refused\npod: {}\nreason: worker metadata is missing\n",
"yoi worker delete\nstatus: refused\nworker: {}\nreason: worker metadata is missing\n",
options.name
),
status: WorkerCleanupCliStatus::Failure,
@ -250,7 +250,7 @@ async fn run_delete(
if let Some(reason) = probe.refusal_reason() {
return Ok(WorkerCleanupCliOutput {
stdout: format!(
"yoi worker delete\nstatus: refused\npod: {}\nreason: {}\nsocket: {}\n",
"yoi worker delete\nstatus: refused\nworker: {}\nreason: {}\nsocket: {}\n",
options.name,
reason,
probe.socket_path.display()
@ -290,7 +290,7 @@ async fn run_prune(
data_dir: PathBuf,
runtime_dir: PathBuf,
) -> Result<WorkerCleanupCliOutput, WorkerCleanupCliError> {
let store = FsWorkerStore::new(data_dir.join("pods")).map_err(to_error)?;
let store = FsWorkerStore::new(data_dir.join("workers")).map_err(to_error)?;
let names = store.list_names().map_err(to_error)?;
let cutoff = SystemTime::now()
.checked_sub(options.older_than)
@ -498,7 +498,7 @@ fn prune_help_text() -> &'static str {
#[cfg(test)]
mod tests {
use super::*;
use pod_store::WorkerActiveSegmentRef;
use session_store::WorkerActiveSegmentRef;
use session_store::{Store, new_segment_id, new_session_id};
fn string_args(args: &[&str]) -> Vec<String> {
@ -543,14 +543,14 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let data_dir = tmp.path().join("data");
let runtime_dir = tmp.path().join("run");
let pod_store = FsWorkerStore::new(data_dir.join("pods")).unwrap();
let worker_metadata_store = FsWorkerStore::new(data_dir.join("workers")).unwrap();
let session_store = session_store::FsStore::new(data_dir.join("sessions")).unwrap();
let session_id = new_session_id();
let segment_id = new_segment_id();
session_store
.create_segment(session_id, segment_id, &[])
.unwrap();
pod_store
worker_metadata_store
.write(&WorkerMetadata::new(
"agent",
Some(WorkerActiveSegmentRef::active_segment(
@ -573,7 +573,12 @@ mod tests {
assert_eq!(output.status, WorkerCleanupCliStatus::Success);
assert!(output.stdout.contains("deleted: worker metadata"));
assert!(pod_store.read_by_name("agent").unwrap().is_none());
assert!(
worker_metadata_store
.read_by_name("agent")
.unwrap()
.is_none()
);
assert!(session_store.exists(session_id, segment_id).unwrap());
}
@ -582,8 +587,8 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let data_dir = tmp.path().join("data");
let runtime_dir = tmp.path().join("run");
let pod_store = FsWorkerStore::new(data_dir.join("pods")).unwrap();
pod_store
let worker_metadata_store = FsWorkerStore::new(data_dir.join("workers")).unwrap();
worker_metadata_store
.write(&WorkerMetadata::new("agent", None))
.unwrap();
@ -601,7 +606,12 @@ mod tests {
assert_eq!(output.status, WorkerCleanupCliStatus::Success);
assert!(output.stdout.contains("mode: dry-run"));
assert!(pod_store.read_by_name("agent").unwrap().is_some());
assert!(
worker_metadata_store
.read_by_name("agent")
.unwrap()
.is_some()
);
}
#[cfg(unix)]
@ -612,8 +622,8 @@ mod tests {
let tmp = tempfile::TempDir::new().unwrap();
let data_dir = tmp.path().join("data");
let runtime_dir = tmp.path().join("run");
let pod_store = FsWorkerStore::new(data_dir.join("pods")).unwrap();
pod_store
let worker_metadata_store = FsWorkerStore::new(data_dir.join("workers")).unwrap();
worker_metadata_store
.write(&WorkerMetadata::new("agent", None))
.unwrap();
std::fs::create_dir_all(runtime_dir.join("agent")).unwrap();
@ -634,6 +644,11 @@ mod tests {
drop(listener);
assert_eq!(output.status, WorkerCleanupCliStatus::Failure);
assert!(output.stdout.contains("status: refused"));
assert!(pod_store.read_by_name("agent").unwrap().is_some());
assert!(
worker_metadata_store
.read_by_name("agent")
.unwrap()
.is_some()
);
}
}

View File

@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter;
};
cargoHash = "sha256-1jSDcivotZ0/v5AURQaetn9xjH5JyQNDeNlJ4AcwEUc=";
cargoHash = "sha256-9e99NfbErWlmyZqXd7d5UaJ88gx6ENbHOubqYtnjXVg=";
depsExtraArgs = {
# Older fetchCargoVendor utilities used crates.io's API download endpoint,

View File

@ -1350,8 +1350,9 @@ impl FixtureWorkspace {
};
fixture.write_fixture_metadata("created", None)?;
write_blocking_pod_metadata(&fixture.xdg_data_home, "workspace")?;
write_blocking_pod_metadata(&fixture.xdg_data_home, "workspace-orchestrator")?;
let worker_metadata_root = active_worker_metadata_root(&fixture.home);
write_blocking_worker_metadata(&worker_metadata_root, "workspace")?;
write_blocking_worker_metadata(&worker_metadata_root, "workspace-orchestrator")?;
run_yoi(
binary,
&fixture.workspace,
@ -1621,8 +1622,8 @@ impl FixtureWorkspace {
"host_runtime_inherited": false,
"host_xdg_runtime_dir_present": std::env::var_os("XDG_RUNTIME_DIR").is_some(),
"tested_yoi_runtime_source": "fixture XDG_RUNTIME_DIR",
"tested_yoi_pod_registry": self.xdg_runtime_dir.join("yoi").join("pods.json"),
"fixture_pod_metadata_root": self.xdg_data_home.join("yoi").join("pods")
"tested_yoi_worker_allocation": self.xdg_runtime_dir.join("yoi").join("workers.json"),
"fixture_worker_metadata_root": active_worker_metadata_root(&self.home)
},
"tested_yoi_env_policy": tested_yoi_env_policy_overview(),
"cleanup": cleanup,
@ -1997,8 +1998,12 @@ fn copy_dir_recursive(source: &Path, destination: &Path) -> Result<()> {
Ok(())
}
fn write_blocking_pod_metadata(data_home: &Path, worker_name: &str) -> Result<()> {
let dir = data_home.join("yoi").join("pods").join(worker_name);
fn active_worker_metadata_root(home: &Path) -> PathBuf {
home.join(".yoi").join("workers")
}
fn write_blocking_worker_metadata(worker_metadata_root: &Path, worker_name: &str) -> Result<()> {
let dir = worker_metadata_root.join(worker_name);
fs::create_dir_all(&dir)?;
fs::write(dir.join("metadata.json"), b"not valid metadata for e2e\n")?;
Ok(())