merge: orchestration runtime updates

This commit is contained in:
Keisuke Hirata 2026-06-26 05:10:58 +09:00
commit 15abcf6782
No known key found for this signature in database
26 changed files with 3245 additions and 9 deletions

View File

@ -1,8 +1,8 @@
--- ---
title: 'worker-runtime core crateと組み込みRuntime APIを作る' title: 'worker-runtime core crateと組み込みRuntime APIを作る'
state: 'inprogress' state: 'done'
created_at: '2026-06-25T12:17:05Z' created_at: '2026-06-25T12:17:05Z'
updated_at: '2026-06-25T16:46:17Z' updated_at: '2026-06-25T16:54:19Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-25T16:20:10Z' queued_at: '2026-06-25T16:20:10Z'

View File

@ -555,3 +555,154 @@ Review result: request_changes
cargo/nix の再実行は、read-only 指示と上記 blocker があるため実施していません。 cargo/nix の再実行は、read-only 指示と上記 blocker があるため実施していません。
--- ---
<!-- event: decision author: yoi-orchestrator at: 2026-06-25T16:49:53Z -->
## Decision
Human follow-up:
ユーザー確認: 「OK、よろしく」。
運用判断:
- 現在 queued の dependent Tickets は planning 差し戻し不要として扱う。
- ただし、未解消 dependency があるものは queued のまま待機し、依存が解消したものだけ再 routing して `queued -> inprogress` を個別に記録してから implementation side effect に進む。
- 途中で concrete missing decision / information が出た場合は、勝手に固定せず Ticket thread に理由を残して停止または planning return を行う。
- 現在の最優先は `00001KVZBCQH4` の reviewer blocker 解消、再 review、merge、validation、done 記録。
---
<!-- event: implementation_report author: yoi-coder-00001KVZBCQH4-worker-runtime at: 2026-06-25T16:50:47Z -->
## Implementation report
Follow-up implementation report after review request_changes:
対応内容:
- `crates/worker-runtime/src/runtime.rs`
- `Runtime::transition_worker` を修正。
- `Stopped` / `Cancelled` の terminal 状態にある Worker に反対側 terminal 操作が呼ばれても、既存 terminal 状態を保持し、同じ `event_id` で ack を返すようにした。
- 反対側 terminal 操作では追加 event を発生させない。
- 追加テスト:
- `stop_then_cancel_preserves_stopped_terminal_state`
- `cancel_then_stop_preserves_cancelled_terminal_state`
- summary count と event history が反対 terminal 操作で変化しないことを検証。
Commit:
- `fbd358a1 fix: keep worker terminal lifecycle stable`
Validation:
- `cargo fmt --all`: success
- `cargo test -p worker-runtime`: success (8 tests passed)
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success実行時点で dirty tree warning は出たが build 自体は成功)
Residual risks / review focus:
- terminal 状態への反対操作は reject ではなく「既存 terminal 状態を保持する idempotent 扱い」を選択。既存 API の ack semantics には自然だが、将来 UX/API で明示的 lifecycle conflict を出したい場合は typed error 方針へ切り替える余地がある。
- child worktree は commit 後 clean。
---
<!-- event: review author: yoi-reviewer-00001KVZBCQH4-worker-runtime at: 2026-06-25T16:52:45Z status: approve -->
## Review: approve
Review result: approve
確認結果:
- 前回 blocker の terminal lifecycle は解消されています。
- `Runtime::transition_worker` は既に `Stopped` / `Cancelled` の Worker に対して、反対側の terminal 操作を受けても state を上書きせず、既存 `status` / `last_event_id` を返し、新規 event も追加しません。
- 該当: `crates/worker-runtime/src/runtime.rs:353-384`
- 追加テストも妥当です。
- `stop_then_cancel_preserves_stopped_terminal_state`
- `cancel_then_stop_preserves_cancelled_terminal_state`
- summary counts と event history の非変化を確認しており、前回指摘した invariant を直接カバーしています。
- `runtime_id + worker_id` / `WorkerRef` authority 境界に regression は見当たりません。
- scope creep は見当たりません。変更範囲は `worker-runtime` crate、workspace 追加、`package.nix` cargoHash 更新に収まっています。
- `worker-runtime` の依存は `serde` / `thiserror` のみで、HTTP/WS/FS/server/client 系依存の追加はありません。
- `package.nix` の cargoHash 更新は維持されています。
実施した確認:
- `git status`, `git log`, `git show fbd358a1`
- `runtime.rs` の lifecycle 実装・追加テスト読解
- `worker-runtime` crate の依存/API 境界確認
- legacy pod/socket/session authority や HTTP/WS/FS 依存の grep 確認
- `git diff --check f8d3b1cc..HEAD`: success
補足: read-only 指示を優先し、`cargo test` / `nix build` は再実行していません。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T16:53:37Z -->
## Implementation report
Integration / validation report:
Integration:
- Implementation branch `work/00001KVZBCQH4-worker-runtime-core` を orchestration branch に merge 済み。
- Merge commit: `56bdf955 merge: 00001KVZBCQH4 worker runtime core`
Included implementation commits:
- `9b2cae32 feat: add memory worker runtime crate`
- `593db951 fix: update nix cargo hash`
- `fbd358a1 fix: keep worker terminal lifecycle stable`
Validation in Orchestrator worktree:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime`: success8 tests passed
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Review:
- Reviewer approve 済み。前回 blocker だった `stop_worker` / `cancel_worker` terminal lifecycle invariant は解消済み。
Outcome:
- Acceptance criteria を満たしたため `done` へ進める。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T16:53:46Z from: inprogress to: done reason: merged_reviewed_validated field: state -->
## State changed
worker-runtime core crate の実装、review、merge、Orchestrator validation が完了した。
Done evidence:
- Merge commit: `56bdf955 merge: 00001KVZBCQH4 worker runtime core`
- Reviewer approve 済み。
- Orchestrator validation:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime`: success8 tests passed
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Scope:
- `crates/worker-runtime` の memory-backed core Runtime API を追加。
- HTTP/WS/FS/Backend integration/config bundle sync は Non-goals として未実装。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T16:54:19Z -->
## Implementation report
Cleanup report:
- Child Workers stopped and scope reclaimed:
- `yoi-coder-00001KVZBCQH4-worker-runtime`
- `yoi-coder-00001KVZBCQH4-runtime-packaging`
- `yoi-reviewer-00001KVZBCQH4-worker-runtime`
- Child implementation worktree removed:
- `/home/hare/Projects/yoi/.worktree/00001KVZBCQH4-worker-runtime-core`
- Child implementation branch removed:
- `work/00001KVZBCQH4-worker-runtime-core`
Remaining note:
- Historical worker-rename child worktree remains separate and was not touched by this cleanup.
---

View File

@ -1 +1,2 @@
{"id":"orch-plan-20260625-164354-1","ticket_id":"00001KVZKST83","kind":"blocked_by","related_ticket":"00001KVZBCQH4","note":"Queue routing checked after Dashboard Queue. FS store feature depends on worker-runtime core `00001KVZBCQH4`, which is currently inprogress and under review. Do not start FS persistence implementation until core API is reviewed/merged/done.","author":"yoi-orchestrator","at":"2026-06-25T16:43:54Z"} {"id":"orch-plan-20260625-164354-1","ticket_id":"00001KVZKST83","kind":"blocked_by","related_ticket":"00001KVZBCQH4","note":"Queue routing checked after Dashboard Queue. FS store feature depends on worker-runtime core `00001KVZBCQH4`, which is currently inprogress and under review. Do not start FS persistence implementation until core API is reviewed/merged/done.","author":"yoi-orchestrator","at":"2026-06-25T16:43:54Z"}
{"id":"orch-plan-20260625-165525-2","ticket_id":"00001KVZKST83","kind":"accepted_plan","note":"`00001KVZBCQH4` が done になり dependency blocker 解消。Backend Registry foundation Ticket とは主対象が `crates/worker-runtime` vs `crates/workspace-server` で分離できるため並行受理候補。ただし Cargo/package lock conflict は merge時に Orchestrator が解消する。","accepted_plan":{"summary":"worker-runtime core done 後の optional `fs-store` feature slice。core no-default-features を維持しつつ FS persistence backend、layout、atomic writes、bounded reads、corrupt diagnostics、memory/FS tests を追加する。REST/Backend integration は扱わない。","branch":"work/00001KVZKST83-worker-runtime-fs-store","worktree":"/home/hare/Projects/yoi/.worktree/00001KVZKST83-worker-runtime-fs-store","role_plan":"Orchestrator が dedicated child worktree を作成し、coder Worker に `crates/worker-runtime` と lock/package files の write scope を委譲する。reviewer Worker は read-only で feature gating、atomic/corrupt diagnostics、runtime_id+worker_id authority、no standalone worker-store を確認する。merge/validation/done/cleanup は Orchestrator が行う。"},"author":"yoi-orchestrator","at":"2026-06-25T16:55:25Z"}

View File

@ -1,8 +1,8 @@
--- ---
title: 'worker-runtimeにFS永続化featureを追加する' title: 'worker-runtimeにFS永続化featureを追加する'
state: 'queued' state: 'done'
created_at: '2026-06-25T14:44:02Z' created_at: '2026-06-25T14:44:02Z'
updated_at: '2026-06-25T16:44:04Z' updated_at: '2026-06-25T19:49:00Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-25T16:39:26Z' queued_at: '2026-06-25T16:39:26Z'

View File

@ -59,3 +59,260 @@ Escalate if:
- FS feature を core Ticket に巻き戻す必要が出る。 - FS feature を core Ticket に巻き戻す必要が出る。
--- ---
<!-- event: decision author: yoi-orchestrator at: 2026-06-25T16:55:42Z -->
## Decision
Routing decision: implementation_ready
Reason:
- `00001KVZBCQH4` worker-runtime core は done。dependency blocker は解消済み。
- 本 Ticket は optional FS persistence feature であり、REST server / Backend Registry integration とは分離されている。
- queued/inprogress 再確認時点で `00001KVZKSV6C` を受理したが、主変更面は Backend Registry foundation (`crates/workspace-server`) と FS feature (`crates/worker-runtime`) で分離できる。Cargo/package files の機械的 conflict は Orchestrator merge時に解消可能と判断する。
Evidence checked:
- Ticket body: `fs-store` feature、runtime/worker scoped layout、atomic write、corrupt diagnostics、bounded reads、Non-goals。
- Relations: outgoing dependency `00001KVZBCQH4` は done。incoming remote Runtime Ticket は後続であり blocker ではない。
- Orchestration plan: accepted plan `orch-plan-20260625-165525-2` を記録。
- Workspace state: orchestration worktree clean。worker-runtime core merge/validation/done/cleanup 済み。
IntentPacket:
Intent:
- `worker-runtime` に optional `fs-store` feature と filesystem persistence backend を追加する。
Binding decisions / invariants:
- Feature disabled 時に core library は FS store dependency を強制しない。
- Store authority は `runtime_id + worker_id`。legacy pod path / socket path / session path を authority にしない。
- Standalone `worker-store` crate は作らない。`worker-runtime` 内 feature/module として実装する。
- REST command server / event stream / Backend integration / legacy Pod session migration / SQLite store は実装しない。
- Existing memory store/core tests を壊さない。
Requirements / acceptance criteria:
- `fs-store` feature がある。
- `cargo test -p worker-runtime --no-default-features` が通る。
- `cargo test -p worker-runtime --features fs-store` が通る。
- Worker create/state update/event append/transcript append/bounded read が FS store で動く。
- Runtime/Worker scoped layout、atomic write、directory creation、corrupt/missing diagnostics を扱う。
Implementation latitude:
- FS layout、module split、serialization structs、atomic write helper、diagnostics typeの詳細は Coder が既存 style に合わせて選べる。
- 旧 `pod-store` の atomic write pattern は参考にしてよいが、依存/責務移植は不要。
Escalate if:
- core Runtime store abstraction の大幅 rewrite が必要になる。
- REST/Backend integration を同時実装しないと acceptance を満たせないと判明する。
- `worker-store` standalone crate が必要に見える。
Validation:
- `cargo fmt --all`
- `cargo test -p worker-runtime --no-default-features`
- `cargo test -p worker-runtime --features fs-store`
- `cargo check -p yoi`
- `git diff --check`
- 可能なら `nix build .#yoi --no-link`
Critical risks / reviewer focus:
- Feature gating failure causing core to pull FS dependencies。
- FS layout leaking legacy Pod/session authority。
- Atomic write / corrupt record handling が不十分。
- Memory store semantics regression。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T16:55:50Z from: queued to: inprogress reason: routing_accepted_after_worker_runtime_core_done field: state -->
## State changed
Routing decision: implementation_ready。
Dependency `00001KVZBCQH4` worker-runtime core は done。Ticket body / relations / workspace state / orchestration plan を確認し、optional FS store feature は unblocked と判断した。accepted plan と IntentPacket は thread / orchestration plan に記録済み。
これ以降、worktree creation / coder Worker routing などの implementation side effect に進める。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T16:58:41Z -->
## Implementation report
Implementation startup blocked after acceptance:
- Worktree created:
- `/home/hare/Projects/yoi/.worktree/00001KVZKST83-worker-runtime-fs-store`
- branch `work/00001KVZKST83-worker-runtime-fs-store`
- Coder Worker was not spawned because the immediately preceding SpawnPod attempt for parallel accepted Ticket `00001KVZKSV6C` failed with:
- `yoi: unknown command pod`
Interpretation:
- No implementation code changes have been made in this worktree yet.
- This appears to be the same runtime/launcher compatibility issue after the Pod→Worker CLI rename.
- Because this Ticket was already accepted (`queued -> inprogress`) and worktree creation completed, it remains inprogress with a recovery note rather than being silently returned to queued.
Recovery needed:
- Restart/restore Orchestrator on a consistent runtime binary or repair the launcher compatibility path, then spawn the Coder Worker for this accepted worktree.
- If retry remains impossible, use an explicit human-authorized alternative implementation route.
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T19:24:59Z -->
## Implementation report
Implementation recovery / kickoff:
- User reported the previous SpawnPod failure was caused by a development environment variable forcing child launch from `target` binary. The variable was removed and installed binary is now used.
- Retried Coder Worker spawn for the already accepted worktree; spawn succeeded.
Active implementation:
- Worktree: `/home/hare/Projects/yoi/.worktree/00001KVZKST83-worker-runtime-fs-store`
- Branch: `work/00001KVZKST83-worker-runtime-fs-store`
- Coder Worker: `yoi-coder-00001KVZKST83-fs-store`
- Scope: read implementation worktree + non-recursive workspace root, write `crates/worker-runtime/**`, root `Cargo.toml`, `Cargo.lock`, and `package.nix` only.
Next action:
- Wait for Coder implementation report, then route to reviewer.
---
<!-- event: implementation_report author: yoi-coder-00001KVZKST83-fs-store at: 2026-06-25T19:41:50Z -->
## Implementation report
Implementation report:
実装完了。merge/close/cleanup は未実施。
Commit:
- `407134399685537da9590d424df6b1db05402965 feat: add worker runtime fs store`
変更概要:
- `worker-runtime` に optional feature `fs-store = ["dep:serde_json"]` を追加。default feature は空のまま。
- `#[cfg(feature = "fs-store")]``fs_store` module を公開。
- `Runtime::with_fs_store(FsRuntimeStoreOptions)` を追加。
- 既存 store があれば復元。
- なければ Runtime scoped layout を作成して初期化。
- `FsRuntimeStore`, `FsRuntimeStoreOptions` を feature 有効時に re-export。
- `RuntimeBackendKind::FsStore` を feature 有効時のみ追加。
FS store:
- layout は `runtime_id + worker_id` authority ベース。
- `<root>/runtimes/<encoded runtime_id>/runtime.json`
- `<root>/runtimes/<encoded runtime_id>/events.jsonl`
- `<root>/runtimes/<encoded runtime_id>/workers/<encoded worker_id>/worker.json`
- `<root>/runtimes/<encoded runtime_id>/workers/<encoded worker_id>/transcript.jsonl`
- legacy pod path / socket / session path は authority として使用していない。
- Worker create / state update / event append / transcript append を Runtime mutation から永続化。
- snapshot 系は same-directory temp file + `sync_all` + rename の atomic write helper。
- event/transcript は JSONL append。
- restore 時に missing / corrupt / wrong runtime/worker scope を `RuntimeError::{StoreIo, StoreMissing, StoreCorrupt}` として診断。
- `FsRuntimeStore::read_events` / `read_transcript` で bounded read を提供。
テスト追加:
- `fs-store` feature 有効時のみ:
- create/send/stop 後に drop/reopen して worker/event/transcript が復元されること。
- direct store bounded read が Runtime read と一致すること。
- corrupt `runtime.json` と missing `worker.json` が診断されること。
変更ファイル:
- `crates/worker-runtime/Cargo.toml`
- `crates/worker-runtime/src/fs_store.rs`
- `crates/worker-runtime/src/runtime.rs`
- `crates/worker-runtime/src/lib.rs`
- `crates/worker-runtime/src/management.rs`
- `crates/worker-runtime/src/error.rs`
- `Cargo.lock`
- `package.nix`
Validation:
- `cargo fmt --all`: success
- `cargo test -p worker-runtime --no-default-features`: success8 passed
- `cargo test -p worker-runtime --features fs-store`: success10 passed
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Residual risks / review focus:
- 永続化は per-file atomic write / append で、複数ファイルをまたぐ transaction ではない。途中 I/O failure 時の cross-file 整合性は review focus。
- FS store は Runtime API に統合しているが、REST / Backend integration / legacy Pod migration / SQLite は未実装。
- Event/transcript read は bounded output だが、現状は JSONL 全体を読み込んでから slice している。大規模 log での streaming pagination は将来最適化候補。
---
<!-- event: review author: yoi-reviewer-00001KVZKST83-fs-store at: 2026-06-25T19:46:00Z status: approve -->
## Review: approve
Review result: approve
確認結果として、Ticket intent / binding decisions / acceptance criteria を満たしていると判断する。
確認した点:
- `worker-runtime``fs-store = ["dep:serde_json"]` が追加され、`default = []` のまま維持されている。
- `fs_store` module / re-export / `Runtime::with_fs_store` / `RuntimeBackendKind::FsStore` は feature-gated。
- `cargo tree -p worker-runtime --no-default-features``serde_json` が入らないことを確認。`--features fs-store` では `serde_json` のみ追加される。
- FS layout は `runtimes/<encoded runtime_id>/workers/<encoded worker_id>/...` で、pod path / socket path / session path を authority にしていない。
- standalone `worker-store` crate、REST command server、Backend integration、legacy migration、SQLite store の追加は見当たらない。
- Runtime/Worker snapshot、event JSONL、transcript JSONL の永続化が実装され、runtime/worker 作成・state update・event append・transcript append・bounded read を満たしている。
- snapshot 書き込みは same-dir temp + `sync_all` + rename + directory sync。missing/corrupt は `RuntimeError::{StoreIo, StoreMissing, StoreCorrupt}` で診断される。
- memory backend 側は feature-off 時に persistence が no-op となり、既存 memory semantics を保つ構成。
- `package.nix``cargoHash` 更新も確認済み。
- `git diff --check` は問題なし。
補足:
- read-only 指示に従い、差分・実装・依存ツリー・scope creep の静的確認に留めた。`cargo test` / `nix build` は作業ツリーへ build artifact を作るため再実行せず、報告済み validation を前提確認として扱った。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T19:48:51Z -->
## Implementation report
Integration / validation report:
Integration:
- Implementation branch `work/00001KVZKST83-worker-runtime-fs-store` を orchestration branch に merge 済み。
- Merge commit: `36ff7238 merge: 00001KVZKST83 worker runtime fs store`
Included implementation commit:
- `407134399685537da9590d424df6b1db05402965 feat: add worker runtime fs store`
Validation in Orchestrator worktree:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime --no-default-features`: success8 tests passed
- `cargo test -p worker-runtime --features fs-store`: success10 tests passed
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Review:
- Reviewer approve 済み。feature gating、runtime_id/worker_id layout、atomic write/corrupt diagnostics、scope boundary に blocker なし。
Outcome:
- Acceptance criteria を満たしたため `done` へ進める。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T19:49:00Z from: inprogress to: done reason: merged_reviewed_validated field: state -->
## State changed
worker-runtime FS store feature の実装、review、merge、Orchestrator validation が完了した。
Done evidence:
- Merge commit: `36ff7238 merge: 00001KVZKST83 worker runtime fs store`
- Reviewer approve 済み。
- Orchestrator validation:
- `cargo fmt --all --check`: success
- `cargo test -p worker-runtime --no-default-features`: success8 tests passed
- `cargo test -p worker-runtime --features fs-store`: success10 tests passed
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: success
Scope:
- optional `fs-store` feature と FS persistence backend を追加。
- REST command server / Backend integration / legacy migration / SQLite store は Non-goals として未実装。
---

View File

@ -1 +1,2 @@
{"id":"orch-plan-20260625-164410-1","ticket_id":"00001KVZKSTE2","kind":"blocked_by","related_ticket":"00001KVZBCQH4","note":"Queue routing checked after Dashboard Queue. REST command server depends on worker-runtime core `00001KVZBCQH4`, which is currently inprogress and under review. Do not start HTTP/server implementation until core API is reviewed/merged/done.","author":"yoi-orchestrator","at":"2026-06-25T16:44:10Z"} {"id":"orch-plan-20260625-164410-1","ticket_id":"00001KVZKSTE2","kind":"blocked_by","related_ticket":"00001KVZBCQH4","note":"Queue routing checked after Dashboard Queue. REST command server depends on worker-runtime core `00001KVZBCQH4`, which is currently inprogress and under review. Do not start HTTP/server implementation until core API is reviewed/merged/done.","author":"yoi-orchestrator","at":"2026-06-25T16:44:10Z"}
{"id":"orch-plan-20260625-165601-2","ticket_id":"00001KVZKSTE2","kind":"waiting_capacity_note","note":"Core dependency is now done, but this REST/http-server Ticket is left queued in this acceptance pass because FS store and Backend Registry foundation were accepted first. `http-server` is likely to modify `crates/worker-runtime` feature/dependency/package surfaces and conflict with FS store work; start after FS store branch stabilizes or Orchestrator explicitly serializes merge conflict handling.","author":"yoi-orchestrator","at":"2026-06-25T16:56:01Z"}

View File

@ -2,7 +2,7 @@
title: 'worker-runtimeにREST command serverを追加する' title: 'worker-runtimeにREST command serverを追加する'
state: 'queued' state: 'queued'
created_at: '2026-06-25T14:44:02Z' created_at: '2026-06-25T14:44:02Z'
updated_at: '2026-06-25T16:44:20Z' updated_at: '2026-06-25T16:56:01Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-25T16:39:39Z' queued_at: '2026-06-25T16:39:39Z'

View File

@ -1 +1,2 @@
{"id":"orch-plan-20260625-163206-1","ticket_id":"00001KVZKSV6C","kind":"blocked_by","related_ticket":"00001KVZBCQH4","note":"Queue routing checked after Dashboard Queue. This Backend RuntimeRegistry foundation Ticket depends on `00001KVZBCQH4` worker-runtime core. That dependency is currently inprogress and only at coder implementation report stage, not reviewed/merged/done, so implementation side effects for this Ticket are blocked.","author":"yoi-orchestrator","at":"2026-06-25T16:32:06Z"} {"id":"orch-plan-20260625-163206-1","ticket_id":"00001KVZKSV6C","kind":"blocked_by","related_ticket":"00001KVZBCQH4","note":"Queue routing checked after Dashboard Queue. This Backend RuntimeRegistry foundation Ticket depends on `00001KVZBCQH4` worker-runtime core. That dependency is currently inprogress and only at coder implementation report stage, not reviewed/merged/done, so implementation side effects for this Ticket are blocked.","author":"yoi-orchestrator","at":"2026-06-25T16:32:06Z"}
{"id":"orch-plan-20260625-165451-2","ticket_id":"00001KVZKSV6C","kind":"accepted_plan","note":"`00001KVZBCQH4` が done になり dependency blocker 解消。FS store Ticket とは主対象が workspace-server vs worker-runtime feature で分離できるため並行受理候補。","accepted_plan":{"summary":"worker-runtime core done 後の Backend RuntimeRegistry foundation slice。embedded/remote 実 handle は作らず、workspace-server の Registry identity/projection/error boundary と local compatibility source naming/diagnostics を worker-runtime domain model に合わせる。","branch":"work/00001KVZKSV6C-backend-runtime-registry","worktree":"/home/hare/Projects/yoi/.worktree/00001KVZKSV6C-backend-runtime-registry","role_plan":"Orchestrator が dedicated child worktree を作成し、coder Worker に `crates/workspace-server` 中心の narrow write scope を委譲する。reviewer Worker は read-only で Registry identity/projection/error boundary と existing API behavior preservation を確認する。merge/validation/done/cleanup は Orchestrator が行う。"},"author":"yoi-orchestrator","at":"2026-06-25T16:54:51Z"}

View File

@ -1,8 +1,8 @@
--- ---
title: 'Backend RuntimeRegistryの基盤をworker-runtime向けに整理する' title: 'Backend RuntimeRegistryの基盤をworker-runtime向けに整理する'
state: 'queued' state: 'inprogress'
created_at: '2026-06-25T14:44:03Z' created_at: '2026-06-25T14:44:03Z'
updated_at: '2026-06-25T16:32:17Z' updated_at: '2026-06-25T19:37:41Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-25T16:31:28Z' queued_at: '2026-06-25T16:31:28Z'

View File

@ -80,3 +80,186 @@ Escalate if:
- Backend Registry foundation 側で worker-runtime core の追加変更が必要になる。 - Backend Registry foundation 側で worker-runtime core の追加変更が必要になる。
--- ---
<!-- event: decision author: yoi-orchestrator at: 2026-06-25T16:55:09Z -->
## Decision
Routing decision: implementation_ready
Reason:
- `00001KVZBCQH4` worker-runtime core は done。dependency blocker は解消済み。
- 本 Ticket の目的は Backend RuntimeRegistry foundation の整理で、embedded runtime 接続や REST/FS 実装そのものではない。
- queued/inprogress 再確認時点で `inprogress` は 0 件。FS store Ticket とは主変更面が `crates/workspace-server` vs `crates/worker-runtime` feature で概ね分離できるため、本 Ticket は受理可能。
Evidence checked:
- Ticket body: Backend RuntimeRegistry foundation、runtime_id / worker_id authority、existing local compatibility source、workspace-server hosts/server target、Non-goals。
- Relations: outgoing dependency `00001KVZBCQH4` は done。incoming dependent `00001KVZSGT0Q`, `00001KVZSGT14` は後続であり blocker ではない。
- Orchestration plan: accepted plan `orch-plan-20260625-165451-2` を記録。
- Workspace state: orchestration worktree clean。worker-runtime core merge/validation/done/cleanup 済み。
IntentPacket:
Intent:
- Backend `RuntimeRegistry` の domain boundary を、legacy process/source abstraction から worker-runtime を受けられる形へ整理する。
Binding decisions / invariants:
- Backend Registry の authority は Runtime / Worker domain identity を扱い、raw socket/session/path/pod name を public authority にしない。
- この Ticket では embedded `worker_runtime::Runtime` を実際に接続しない。handle/trait/enum/boundary と diagnostics/projection 整理まで。
- Remote Runtime HTTP client / REST server / Web Console / dynamic Runtime registration は実装しない。
- 既存 local compatibility source の behavior は壊さない。
- `worker-runtime` core crate の API を大きく変更しない。必要になれば escalation。
Requirements / acceptance criteria:
- `workspace-server` の RuntimeRegistry foundation が worker-runtime 向けの identity / projection / error boundary を持つ。
- Existing local/process compatibility source と将来 embedded/remote source の境界が型で分かる。
- Browser/API response へ socket/session/path authority を露出しない。
- Existing workspace-server tests が通る。
Implementation latitude:
- Type/module split、内部 trait/enum naming、diagnostic representation は既存 `crates/workspace-server/src/hosts.rs` の形に合わせてよい。
- Minimal compile/test slice でよい。実 embedded handle は placeholder/boundary でよい。
Escalate if:
- `worker-runtime` crate の public API 変更が必要になる。
- Backend embedded connection を実装しないと acceptance を満たせないと判明する。
- API response schema 互換性や dashboard expectations を大きく変える必要がある。
Validation:
- `cargo fmt --all`
- `cargo test -p yoi-workspace-server`
- `cargo check -p yoi`
- `git diff --check`
- 可能なら `nix build .#yoi --no-link`
Critical risks / reviewer focus:
- Registry boundary が legacy Pod/socket/session authority を引きずること。
- `00001KVZSGT0Q` の実 embedded接続まで scope creep すること。
- Existing local compatibility source の API/diagnostics regression。
---
<!-- event: state_changed author: yoi-orchestrator at: 2026-06-25T16:55:16Z from: queued to: inprogress reason: routing_accepted_after_worker_runtime_core_done field: state -->
## State changed
Routing decision: implementation_ready。
Dependency `00001KVZBCQH4` worker-runtime core は done。Ticket body / relations / workspace state / orchestration plan を確認し、Backend RuntimeRegistry foundation は unblocked と判断した。accepted plan と IntentPacket は thread / orchestration plan に記録済み。
これ以降、worktree creation / coder Worker routing などの implementation side effect に進める。
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T16:58:32Z -->
## Implementation report
Implementation startup blocked after acceptance:
- Worktree created:
- `/home/hare/Projects/yoi/.worktree/00001KVZKSV6C-backend-runtime-registry`
- branch `work/00001KVZKSV6C-backend-runtime-registry`
- Coder Worker spawn attempted:
- `yoi-coder-00001KVZKSV6C-runtime-registry`
- Spawn failed before child socket appeared:
- `yoi: unknown command pod`
Interpretation:
- No implementation code changes have been made in this worktree yet.
- This appears to be a runtime/launcher compatibility issue after the Pod→Worker CLI rename: the SpawnPod tool path attempted to invoke a `pod` subcommand that the child-launch binary rejected.
- Because this failure happened after `queued -> inprogress` acceptance and worktree creation, the Ticket remains inprogress with a recovery note rather than being silently returned to queued.
Recovery needed:
- Restart/restore Orchestrator on a consistent runtime binary or repair the launcher compatibility path, then retry Coder Worker spawn for this accepted worktree.
- If retry remains impossible, use an explicit human-authorized alternative implementation route.
---
<!-- event: implementation_report author: yoi-orchestrator at: 2026-06-25T19:24:51Z -->
## Implementation report
Implementation recovery / kickoff:
- User reported the previous SpawnPod failure was caused by a development environment variable forcing child launch from `target` binary. The variable was removed and installed binary is now used.
- Retried Coder Worker spawn for the already accepted worktree; spawn succeeded.
Active implementation:
- Worktree: `/home/hare/Projects/yoi/.worktree/00001KVZKSV6C-backend-runtime-registry`
- Branch: `work/00001KVZKSV6C-backend-runtime-registry`
- Coder Worker: `yoi-coder-00001KVZKSV6C-runtime-registry`
- Scope: read implementation worktree + non-recursive workspace root, write `crates/workspace-server/**` only.
Next action:
- Wait for Coder implementation report, then route to reviewer.
---
<!-- event: implementation_report author: yoi-coder-00001KVZKSV6C-runtime-registry at: 2026-06-25T19:33:11Z -->
## Implementation report
Implementation report:
実装完了。Ticket ファイル/project records は変更していない。merge/close/cleanup 前で停止。
Commit:
- `f6fd7b63 feat: add workspace runtime registry source boundary`
変更ファイル:
- `crates/workspace-server/src/hosts.rs`
- `crates/workspace-server/src/server.rs`
実装/API 概要:
- `WorkerRuntimeRegistry``RuntimeRegistry` に改名し、workspace-server 内の Runtime/Worker domain 側の registry 名を明確化。
- `RuntimeSummary``source: RuntimeSourceSummary` を追加。
- `RuntimeSourceKind`: `local_compatibility`, `embedded_worker_runtime`, `remote_http`
- `RuntimeSourceStatus`: `active`, `reserved`
- `RuntimeIdentityAuthority`: `runtime_registry_projection`
- 現行 local Pod metadata 由来の互換 source は `local_compatibility` として明示。
- embedded worker-runtime / remote HTTP は reserved boundary として型だけ用意し、実接続・HTTP client/server・dynamic registration は未実装のまま保持。
- API response に source boundary / identity authority を追加し、Runtime public identity が socket/session/path ではなく registry projection であることをテストで確認。
- 既存 local compatibility behavior は維持し、既存テストに source boundary の assert を追加。
Validation:
- `cargo fmt --all`: success
- `cargo test -p yoi-workspace-server`: success19 passed
- `cargo check -p yoi`: success
- `git diff --check`: success
- `nix build .#yoi --no-link`: successdirty warning は出たが build 成功)
Residual risks / review focus:
- API top-level `source: "worker_runtime_registry"` は既存互換のため維持。一方で Runtime item 側に typed `source.kind` / `identity_authority` を追加。
- embedded/remote source は reserved constructor と enum boundary のみで、実際の registry composition や Runtime 接続は次 Ticket 側の責務。
- `RuntimeSourceSummary.note` は human-readable な API payload なので、Web 側で表示するなら文言の粒度を確認してほしい。
---
<!-- event: review author: yoi-reviewer-00001KVZKSV6C-runtime-registry at: 2026-06-25T19:37:41Z status: request_changes -->
## Review: request changes
Review result: request_changes
ブロッカー:
1. Runtime/Worker authority がまだ `runtime_id + worker_id` になっていない。
- `RuntimeRegistry::worker(&self, worker_id)``runtime_id` を受け取らず、全 Runtime を `worker_id` だけで走査している。
- `RuntimeRegistryError` にも `UnknownRuntime` 相当がない。
- Ticket の binding decision / 要件では Worker authority は `runtime_id + worker_id`、unknown runtime / unknown worker 等の error boundary を typed に分ける必要がある。
- 将来 embedded / remote source が増えた時に `worker_id` 衝突や誤 routing が起き得るため、foundation として不足。
- 該当: `crates/workspace-server/src/hosts.rs` `RuntimeRegistry::worker` / `RuntimeRegistryError`
2. runtime-scoped routing/error mapping のテストが不足している。
- 追加テストは `local_compatibility` source / identity_authority / path 非露出は確認しているが、複数 Runtime または unknown runtime を想定した `runtime_id` scoped lookup / error boundary を確認していない。
- Acceptance の “Runtime identity / Worker routing key / error boundary” を満たすには、typed `runtime_id` lookup と unknown-runtime / unknown-worker の focused test が必要。
確認できた良い点:
- `RuntimeSourceKind::{LocalCompatibility, EmbeddedWorkerRuntime, RemoteHttp}``RuntimeSourceSummary` により、local compatibility / future embedded / future remote の source 境界は型として追加されている。
- local source は `source.kind = local_compatibility`、`identity_authority = runtime_registry_projection` として API/test で確認されている。
- socket/session/path authority の直接公開や embedded `worker_runtime::Runtime` 接続、remote HTTP client/server 実装などの scope creep は見当たらない。
- 変更範囲は `workspace-server``hosts.rs` / `server.rs` のみで、`worker-runtime` public API 変更は見当たらない。
- read-only で `git diff --check f6fd7b63^ f6fd7b63` は問題なし。Cargo/Nix 検証は read-only 指示を優先して再実行していない。
---

View File

@ -1 +1,2 @@
{"id":"orch-plan-20260625-164457-1","ticket_id":"00001KVZQHPNY","kind":"blocked_by","related_ticket":"00001KVZBCQH4","note":"Queue routing checked after Dashboard Queue. Profile/config bundle sync depends on worker-runtime core `00001KVZBCQH4`, which is currently inprogress and under review. Do not start sync implementation until core CreateWorkerRequest/Profile boundary is reviewed/merged/done.","author":"yoi-orchestrator","at":"2026-06-25T16:44:57Z"} {"id":"orch-plan-20260625-164457-1","ticket_id":"00001KVZQHPNY","kind":"blocked_by","related_ticket":"00001KVZBCQH4","note":"Queue routing checked after Dashboard Queue. Profile/config bundle sync depends on worker-runtime core `00001KVZBCQH4`, which is currently inprogress and under review. Do not start sync implementation until core CreateWorkerRequest/Profile boundary is reviewed/merged/done.","author":"yoi-orchestrator","at":"2026-06-25T16:44:57Z"}
{"id":"orch-plan-20260625-165606-2","ticket_id":"00001KVZQHPNY","kind":"waiting_capacity_note","note":"Core dependency is now done, but this bundle-sync Ticket is left queued in this acceptance pass because Backend Registry foundation and FS store were accepted first. Bundle sync likely touches `worker-runtime` creation/profile boundary and Backend Registry availability semantics, so it should start after at least the foundation branch shape is reviewed or merged to avoid design/API churn.","author":"yoi-orchestrator","at":"2026-06-25T16:56:06Z"}

View File

@ -2,7 +2,7 @@
title: 'RuntimeへProfile/config bundleを同期する' title: 'RuntimeへProfile/config bundleを同期する'
state: 'queued' state: 'queued'
created_at: '2026-06-25T15:49:30Z' created_at: '2026-06-25T15:49:30Z'
updated_at: '2026-06-25T16:45:08Z' updated_at: '2026-06-25T16:56:06Z'
assignee: null assignee: null
queued_by: 'workspace-panel' queued_by: 'workspace-panel'
queued_at: '2026-06-25T16:44:39Z' queued_at: '2026-06-25T16:44:39Z'

9
Cargo.lock generated
View File

@ -5901,6 +5901,15 @@ dependencies = [
"yoi-plugin-pdk", "yoi-plugin-pdk",
] ]
[[package]]
name = "worker-runtime"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "workflow" name = "workflow"
version = "0.1.0" version = "0.1.0"

View File

@ -9,6 +9,7 @@ members = [
"crates/manifest", "crates/manifest",
"crates/mcp", "crates/mcp",
"crates/worker", "crates/worker",
"crates/worker-runtime",
"crates/plugin-pdk", "crates/plugin-pdk",
"crates/yoi", "crates/yoi",
"crates/pod-store", "crates/pod-store",
@ -36,6 +37,7 @@ default-members = [
"crates/manifest", "crates/manifest",
"crates/mcp", "crates/mcp",
"crates/worker", "crates/worker",
"crates/worker-runtime",
"crates/plugin-pdk", "crates/plugin-pdk",
"crates/yoi", "crates/yoi",
"crates/pod-store", "crates/pod-store",
@ -70,6 +72,7 @@ memory = { path = "crates/memory" }
ticket = { path = "crates/ticket" } ticket = { path = "crates/ticket" }
project-record = { path = "crates/project-record" } project-record = { path = "crates/project-record" }
worker = { path = "crates/worker" } worker = { path = "crates/worker" }
worker-runtime = { path = "crates/worker-runtime" }
yoi-plugin-pdk = { path = "crates/plugin-pdk" } yoi-plugin-pdk = { path = "crates/plugin-pdk" }
yoi = { path = "crates/yoi" } yoi = { path = "crates/yoi" }
pod-registry = { path = "crates/pod-registry" } pod-registry = { path = "crates/pod-registry" }

View File

@ -0,0 +1,15 @@
[package]
name = "worker-runtime"
description = "Embedded memory-backed Runtime API for Worker management"
version = "0.1.0"
edition.workspace = true
license.workspace = true
[features]
default = []
fs-store = ["dep:serde_json"]
[dependencies]
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, optional = true }
thiserror = { workspace = true }

View File

@ -0,0 +1,179 @@
use crate::identity::{RuntimeId, WorkerId, WorkerRef};
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.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum ProfileSelector {
RuntimeDefault,
Builtin(String),
Named(String),
}
impl Default for ProfileSelector {
fn default() -> Self {
Self::RuntimeDefault
}
}
/// Placeholder for future config-bundle synchronization.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConfigBundleRef {
pub id: 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.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreateWorkerRequest {
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>,
}
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(),
}
}
}
/// Worker lifecycle status for the in-memory embedded runtime.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkerStatus {
Running,
Stopped,
Cancelled,
}
impl WorkerStatus {
pub fn is_active(self) -> bool {
matches!(self, Self::Running)
}
}
/// Lightweight catalog row.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerSummary {
pub worker_ref: WorkerRef,
pub runtime_id: RuntimeId,
pub worker_id: WorkerId,
pub status: WorkerStatus,
pub intent: WorkerIntent,
pub profile: ProfileSelector,
pub requested_capability_count: usize,
pub has_config_bundle: bool,
pub transcript_len: usize,
pub last_event_id: u64,
}
/// Full Worker catalog/lifecycle detail.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerDetail {
pub worker_ref: WorkerRef,
pub runtime_id: RuntimeId,
pub worker_id: WorkerId,
pub status: WorkerStatus,
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 transcript_len: usize,
pub last_event_id: u64,
}
/// Acknowledgement returned by stop/cancel lifecycle operations.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerLifecycleAck {
pub worker_ref: WorkerRef,
pub status: WorkerStatus,
pub event_id: u64,
}

View File

@ -0,0 +1,21 @@
use crate::identity::WorkerRef;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticSeverity {
Info,
Warning,
Error,
}
/// Runtime diagnostic emitted by memory-runtime operations.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeDiagnostic {
pub id: u64,
pub severity: DiagnosticSeverity,
pub code: String,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worker_ref: Option<WorkerRef>,
}

View File

@ -0,0 +1,60 @@
use crate::identity::{RuntimeId, WorkerId};
use std::path::PathBuf;
/// Errors returned by the embedded Runtime API.
#[derive(Debug, thiserror::Error)]
pub enum RuntimeError {
#[error("runtime {runtime_id} is stopped")]
RuntimeStopped { runtime_id: RuntimeId },
#[error(
"worker {worker_id} belongs to runtime {actual_runtime_id}, not runtime {expected_runtime_id}"
)]
WrongRuntime {
expected_runtime_id: RuntimeId,
actual_runtime_id: RuntimeId,
worker_id: WorkerId,
},
#[error("cursor belongs to runtime {actual_runtime_id}, not runtime {expected_runtime_id}")]
WrongRuntimeCursor {
expected_runtime_id: RuntimeId,
actual_runtime_id: RuntimeId,
},
#[error("worker {worker_id} was not found in runtime {runtime_id}")]
WorkerNotFound {
runtime_id: RuntimeId,
worker_id: WorkerId,
},
#[error("limit {requested} exceeds maximum {max}")]
LimitTooLarge { requested: usize, max: usize },
#[error("invalid request: {0}")]
InvalidRequest(String),
#[error("runtime store {operation} failed at {}: {source}", path.display())]
StoreIo {
operation: &'static str,
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("runtime store {operation} missing data at {}", path.display())]
StoreMissing {
operation: &'static str,
path: PathBuf,
},
#[error("runtime store {operation} found corrupt data at {}: {message}", path.display())]
StoreCorrupt {
operation: &'static str,
path: PathBuf,
message: String,
},
#[error("runtime state lock was poisoned")]
StatePoisoned,
}

View File

@ -0,0 +1,755 @@
use crate::catalog::{CreateWorkerRequest, WorkerStatus};
use crate::diagnostics::RuntimeDiagnostic;
use crate::error::RuntimeError;
use crate::identity::{RuntimeId, WorkerId, WorkerRef};
use crate::management::{RuntimeBackendKind, RuntimeLimits, RuntimeStatus};
use crate::observation::{
EventCursor, RuntimeEvent, RuntimeEventBatch, TranscriptEntry, TranscriptProjection,
TranscriptQuery,
};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs::{self, File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
const SCHEMA_VERSION: u32 = 1;
const RUNTIMES_DIR: &str = "runtimes";
const RUNTIME_FILE: &str = "runtime.json";
const EVENTS_FILE: &str = "events.jsonl";
const WORKERS_DIR: &str = "workers";
const WORKER_FILE: &str = "worker.json";
const TRANSCRIPT_FILE: &str = "transcript.jsonl";
static NEXT_TMP_SEQUENCE: AtomicU64 = AtomicU64::new(1);
/// Options for constructing a filesystem-backed Runtime store.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FsRuntimeStoreOptions {
/// Root directory containing all Runtime-scoped store data.
pub root: PathBuf,
pub runtime_id: Option<RuntimeId>,
pub display_name: Option<String>,
pub limits: RuntimeLimits,
}
impl FsRuntimeStoreOptions {
pub fn new(root: impl Into<PathBuf>) -> Self {
Self {
root: root.into(),
runtime_id: None,
display_name: None,
limits: RuntimeLimits::default(),
}
}
}
/// Filesystem persistence boundary for Worker Runtime state.
///
/// Authority is the typed `runtime_id + worker_id` pair. Those ids are encoded
/// into path components only after validation; legacy pod paths, socket paths,
/// and session paths are deliberately not part of the layout or lookup API.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FsRuntimeStore {
root: PathBuf,
runtime_id: RuntimeId,
runtime_dir: PathBuf,
}
impl FsRuntimeStore {
pub fn root(&self) -> &Path {
&self.root
}
pub fn runtime_id(&self) -> &RuntimeId {
&self.runtime_id
}
pub fn runtime_dir(&self) -> &Path {
&self.runtime_dir
}
/// Read persisted Runtime events directly from the event log with the same
/// bounded cursor semantics as [`crate::Runtime::read_events`].
pub fn read_events(
&self,
cursor: &EventCursor,
limit: usize,
max_limit: usize,
) -> Result<RuntimeEventBatch, RuntimeError> {
if cursor.runtime_id != self.runtime_id {
return Err(RuntimeError::WrongRuntimeCursor {
expected_runtime_id: self.runtime_id.clone(),
actual_runtime_id: cursor.runtime_id.clone(),
});
}
if limit > max_limit {
return Err(RuntimeError::LimitTooLarge {
requested: limit,
max: max_limit,
});
}
let events = read_json_lines::<RuntimeEvent>(&self.events_path(), "read events")?;
let mut selected = Vec::new();
for event in events
.iter()
.filter(|event| event.id >= cursor.next_event_id)
.take(limit)
{
selected.push(event.clone());
}
let next_event_id = selected
.last()
.map(|event| event.id + 1)
.unwrap_or(cursor.next_event_id);
let has_more = events.iter().any(|event| event.id >= next_event_id);
Ok(RuntimeEventBatch {
runtime_id: self.runtime_id.clone(),
cursor: EventCursor {
runtime_id: self.runtime_id.clone(),
next_event_id,
},
events: selected,
has_more,
})
}
/// Read a persisted Worker transcript directly from its Worker-scoped log.
pub fn read_transcript(
&self,
worker_ref: &WorkerRef,
query: TranscriptQuery,
max_limit: usize,
) -> Result<TranscriptProjection, RuntimeError> {
self.ensure_worker_ref(worker_ref)?;
if query.limit > max_limit {
return Err(RuntimeError::LimitTooLarge {
requested: query.limit,
max: max_limit,
});
}
let path = self.transcript_path(&worker_ref.worker_id);
let entries = read_json_lines::<TranscriptEntry>(&path, "read transcript")?;
let total_items = entries.len();
let end = query.start.saturating_add(query.limit).min(total_items);
let items = if query.start >= total_items {
Vec::new()
} else {
entries[query.start..end].to_vec()
};
let next_start = (end < total_items).then_some(end);
Ok(TranscriptProjection {
worker_ref: worker_ref.clone(),
start: query.start,
limit: query.limit,
total_items,
items,
next_start,
})
}
pub(crate) fn open_or_create(
root: PathBuf,
runtime_id: RuntimeId,
) -> Result<OpenedFsRuntimeStore, RuntimeError> {
fs::create_dir_all(root.join(RUNTIMES_DIR)).map_err(|source| RuntimeError::StoreIo {
operation: "create store root",
path: root.join(RUNTIMES_DIR),
source,
})?;
let runtime_dir = runtime_dir(&root, &runtime_id);
let existed = runtime_dir.exists();
if existed && !runtime_dir.is_dir() {
return Err(RuntimeError::StoreCorrupt {
operation: "open runtime store",
path: runtime_dir,
message: "runtime path exists but is not a directory".to_string(),
});
}
fs::create_dir_all(runtime_dir.join(WORKERS_DIR)).map_err(|source| {
RuntimeError::StoreIo {
operation: "create runtime store",
path: runtime_dir.join(WORKERS_DIR),
source,
}
})?;
let store = Self {
root,
runtime_id,
runtime_dir,
};
let state = if existed {
Some(store.load_runtime_state()?)
} else {
None
};
Ok(OpenedFsRuntimeStore { store, state })
}
pub(crate) fn write_runtime_snapshot(
&self,
state: &PersistedRuntimeState,
) -> Result<(), RuntimeError> {
let snapshot = RuntimeSnapshot::from_persisted(state);
atomic_write_json(&self.runtime_path(), &snapshot, "write runtime snapshot")
}
pub(crate) fn write_worker_snapshot(
&self,
worker: &PersistedWorkerRecord,
) -> Result<(), RuntimeError> {
self.ensure_worker_ref(&worker.worker_ref)?;
let worker_dir = self.worker_dir(&worker.worker_id);
fs::create_dir_all(&worker_dir).map_err(|source| RuntimeError::StoreIo {
operation: "create worker store",
path: worker_dir.clone(),
source,
})?;
atomic_write_json(
&worker_dir.join(WORKER_FILE),
&WorkerSnapshot::from_persisted(worker),
"write worker snapshot",
)?;
ensure_file_exists(&worker_dir.join(TRANSCRIPT_FILE), "create transcript log")
}
pub(crate) fn append_event(&self, event: &RuntimeEvent) -> Result<(), RuntimeError> {
if let Some(worker_ref) = &event.worker_ref {
self.ensure_worker_ref(worker_ref)?;
}
append_json_line(&self.events_path(), event, "append event")
}
pub(crate) fn append_transcript_entry(
&self,
entry: &TranscriptEntry,
) -> Result<(), RuntimeError> {
self.ensure_worker_ref(&entry.worker_ref)?;
append_json_line(
&self.transcript_path(&entry.worker_ref.worker_id),
entry,
"append transcript",
)
}
pub(crate) fn load_runtime_state(&self) -> Result<PersistedRuntimeState, RuntimeError> {
let runtime_path = self.runtime_path();
let snapshot: RuntimeSnapshot = read_json(&runtime_path, "read runtime snapshot")?;
snapshot.validate(&self.runtime_id, &runtime_path)?;
let events = read_json_lines::<RuntimeEvent>(&self.events_path(), "read events")?;
let workers_dir = self.runtime_dir.join(WORKERS_DIR);
if !workers_dir.exists() {
return Err(RuntimeError::StoreMissing {
operation: "read workers",
path: workers_dir,
});
}
if !workers_dir.is_dir() {
return Err(RuntimeError::StoreCorrupt {
operation: "read workers",
path: workers_dir,
message: "workers path exists but is not a directory".to_string(),
});
}
let mut workers = BTreeMap::new();
let mut worker_dirs = fs::read_dir(&workers_dir)
.map_err(|source| RuntimeError::StoreIo {
operation: "read workers",
path: workers_dir.clone(),
source,
})?
.collect::<Result<Vec<_>, _>>()
.map_err(|source| RuntimeError::StoreIo {
operation: "read workers",
path: workers_dir.clone(),
source,
})?;
worker_dirs.sort_by_key(|entry| entry.path());
for entry in worker_dirs {
let path = entry.path();
if !path.is_dir() {
return Err(RuntimeError::StoreCorrupt {
operation: "read worker",
path,
message: "worker path exists but is not a directory".to_string(),
});
}
let worker_snapshot_path = path.join(WORKER_FILE);
let worker_snapshot: WorkerSnapshot =
read_json(&worker_snapshot_path, "read worker snapshot")?;
worker_snapshot.validate(&self.runtime_id, &worker_snapshot_path)?;
let transcript =
read_json_lines::<TranscriptEntry>(&path.join(TRANSCRIPT_FILE), "read transcript")?;
for entry in &transcript {
self.ensure_worker_ref(&entry.worker_ref)?;
if entry.worker_ref.worker_id != worker_snapshot.worker_id {
return Err(RuntimeError::StoreCorrupt {
operation: "read transcript",
path: path.join(TRANSCRIPT_FILE),
message: format!(
"transcript entry belongs to worker {}, expected {}",
entry.worker_ref.worker_id, worker_snapshot.worker_id
),
});
}
}
let worker = worker_snapshot.into_persisted(transcript);
if workers.insert(worker.worker_id.clone(), worker).is_some() {
return Err(RuntimeError::StoreCorrupt {
operation: "read workers",
path: workers_dir.clone(),
message: "duplicate worker id in store".to_string(),
});
}
}
Ok(snapshot.into_persisted(events, workers))
}
fn ensure_worker_ref(&self, worker_ref: &WorkerRef) -> Result<(), RuntimeError> {
if worker_ref.runtime_id == self.runtime_id {
Ok(())
} else {
Err(RuntimeError::WrongRuntime {
expected_runtime_id: self.runtime_id.clone(),
actual_runtime_id: worker_ref.runtime_id.clone(),
worker_id: worker_ref.worker_id.clone(),
})
}
}
fn runtime_path(&self) -> PathBuf {
self.runtime_dir.join(RUNTIME_FILE)
}
fn events_path(&self) -> PathBuf {
self.runtime_dir.join(EVENTS_FILE)
}
fn worker_dir(&self, worker_id: &WorkerId) -> PathBuf {
self.runtime_dir
.join(WORKERS_DIR)
.join(encoded_component(worker_id.as_str()))
}
fn transcript_path(&self, worker_id: &WorkerId) -> PathBuf {
self.worker_dir(worker_id).join(TRANSCRIPT_FILE)
}
}
#[derive(Debug)]
pub(crate) struct OpenedFsRuntimeStore {
pub(crate) store: FsRuntimeStore,
pub(crate) state: Option<PersistedRuntimeState>,
}
#[derive(Clone, Debug)]
pub(crate) struct PersistedRuntimeState {
pub(crate) runtime_id: RuntimeId,
pub(crate) display_name: Option<String>,
pub(crate) status: RuntimeStatus,
pub(crate) limits: RuntimeLimits,
pub(crate) next_worker_sequence: u64,
pub(crate) next_event_id: u64,
pub(crate) next_diagnostic_id: u64,
pub(crate) workers: BTreeMap<WorkerId, PersistedWorkerRecord>,
pub(crate) events: Vec<RuntimeEvent>,
pub(crate) diagnostics: Vec<RuntimeDiagnostic>,
}
#[derive(Clone, Debug)]
pub(crate) struct PersistedWorkerRecord {
pub(crate) worker_ref: WorkerRef,
pub(crate) worker_id: WorkerId,
pub(crate) status: WorkerStatus,
pub(crate) request: CreateWorkerRequest,
pub(crate) transcript: Vec<TranscriptEntry>,
pub(crate) next_transcript_sequence: u64,
pub(crate) last_event_id: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct RuntimeSnapshot {
schema_version: u32,
runtime_id: RuntimeId,
display_name: Option<String>,
backend: RuntimeBackendKind,
status: RuntimeStatus,
limits: RuntimeLimits,
next_worker_sequence: u64,
next_event_id: u64,
next_diagnostic_id: u64,
diagnostics: Vec<RuntimeDiagnostic>,
}
impl RuntimeSnapshot {
fn from_persisted(state: &PersistedRuntimeState) -> Self {
Self {
schema_version: SCHEMA_VERSION,
runtime_id: state.runtime_id.clone(),
display_name: state.display_name.clone(),
backend: RuntimeBackendKind::FsStore,
status: state.status,
limits: state.limits.clone(),
next_worker_sequence: state.next_worker_sequence,
next_event_id: state.next_event_id,
next_diagnostic_id: state.next_diagnostic_id,
diagnostics: state.diagnostics.clone(),
}
}
fn validate(&self, expected_runtime_id: &RuntimeId, path: &Path) -> Result<(), RuntimeError> {
if self.schema_version != SCHEMA_VERSION {
return Err(RuntimeError::StoreCorrupt {
operation: "read runtime snapshot",
path: path.to_path_buf(),
message: format!(
"unsupported schema version {}, expected {}",
self.schema_version, SCHEMA_VERSION
),
});
}
if &self.runtime_id != expected_runtime_id {
return Err(RuntimeError::StoreCorrupt {
operation: "read runtime snapshot",
path: path.to_path_buf(),
message: format!(
"runtime snapshot id {} does not match requested runtime {}",
self.runtime_id, expected_runtime_id
),
});
}
if self.backend != RuntimeBackendKind::FsStore {
return Err(RuntimeError::StoreCorrupt {
operation: "read runtime snapshot",
path: path.to_path_buf(),
message: format!("runtime snapshot backend is {:?}", self.backend),
});
}
Ok(())
}
fn into_persisted(
self,
events: Vec<RuntimeEvent>,
workers: BTreeMap<WorkerId, PersistedWorkerRecord>,
) -> PersistedRuntimeState {
PersistedRuntimeState {
runtime_id: self.runtime_id,
display_name: self.display_name,
status: self.status,
limits: self.limits,
next_worker_sequence: self.next_worker_sequence,
next_event_id: self.next_event_id,
next_diagnostic_id: self.next_diagnostic_id,
workers,
events,
diagnostics: self.diagnostics,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct WorkerSnapshot {
schema_version: u32,
worker_ref: WorkerRef,
worker_id: WorkerId,
status: WorkerStatus,
request: CreateWorkerRequest,
next_transcript_sequence: u64,
last_event_id: u64,
}
impl WorkerSnapshot {
fn from_persisted(worker: &PersistedWorkerRecord) -> Self {
Self {
schema_version: SCHEMA_VERSION,
worker_ref: worker.worker_ref.clone(),
worker_id: worker.worker_id.clone(),
status: worker.status,
request: worker.request.clone(),
next_transcript_sequence: worker.next_transcript_sequence,
last_event_id: worker.last_event_id,
}
}
fn validate(&self, expected_runtime_id: &RuntimeId, path: &Path) -> Result<(), RuntimeError> {
if self.schema_version != SCHEMA_VERSION {
return Err(RuntimeError::StoreCorrupt {
operation: "read worker snapshot",
path: path.to_path_buf(),
message: format!(
"unsupported schema version {}, expected {}",
self.schema_version, SCHEMA_VERSION
),
});
}
if self.worker_ref.runtime_id != *expected_runtime_id {
return Err(RuntimeError::StoreCorrupt {
operation: "read worker snapshot",
path: path.to_path_buf(),
message: format!(
"worker belongs to runtime {}, expected {}",
self.worker_ref.runtime_id, expected_runtime_id
),
});
}
if self.worker_ref.worker_id != self.worker_id {
return Err(RuntimeError::StoreCorrupt {
operation: "read worker snapshot",
path: path.to_path_buf(),
message: format!(
"worker_ref id {} does not match worker_id {}",
self.worker_ref.worker_id, self.worker_id
),
});
}
Ok(())
}
fn into_persisted(self, transcript: Vec<TranscriptEntry>) -> PersistedWorkerRecord {
PersistedWorkerRecord {
worker_ref: self.worker_ref,
worker_id: self.worker_id,
status: self.status,
request: self.request,
transcript,
next_transcript_sequence: self.next_transcript_sequence,
last_event_id: self.last_event_id,
}
}
}
fn runtime_dir(root: &Path, runtime_id: &RuntimeId) -> PathBuf {
root.join(RUNTIMES_DIR)
.join(encoded_component(runtime_id.as_str()))
}
fn encoded_component(value: &str) -> String {
let mut encoded = String::with_capacity(3 + value.len() * 2);
encoded.push_str("id-");
for byte in value.as_bytes() {
encoded.push(hex_digit(byte >> 4));
encoded.push(hex_digit(byte & 0x0f));
}
encoded
}
fn hex_digit(value: u8) -> char {
match value {
0..=9 => (b'0' + value) as char,
10..=15 => (b'a' + (value - 10)) as char,
_ => unreachable!("hex digit nybble is always <= 15"),
}
}
fn read_json<T>(path: &Path, operation: &'static str) -> Result<T, RuntimeError>
where
T: for<'de> Deserialize<'de>,
{
let file = File::open(path).map_err(|source| match source.kind() {
std::io::ErrorKind::NotFound => RuntimeError::StoreMissing {
operation,
path: path.to_path_buf(),
},
_ => RuntimeError::StoreIo {
operation,
path: path.to_path_buf(),
source,
},
})?;
serde_json::from_reader(BufReader::new(file)).map_err(|source| RuntimeError::StoreCorrupt {
operation,
path: path.to_path_buf(),
message: source.to_string(),
})
}
fn read_json_lines<T>(path: &Path, operation: &'static str) -> Result<Vec<T>, RuntimeError>
where
T: for<'de> Deserialize<'de>,
{
let file = File::open(path).map_err(|source| match source.kind() {
std::io::ErrorKind::NotFound => RuntimeError::StoreMissing {
operation,
path: path.to_path_buf(),
},
_ => RuntimeError::StoreIo {
operation,
path: path.to_path_buf(),
source,
},
})?;
let reader = BufReader::new(file);
let mut items = Vec::new();
for (index, line) in reader.lines().enumerate() {
let line = line.map_err(|source| RuntimeError::StoreIo {
operation,
path: path.to_path_buf(),
source,
})?;
if line.trim().is_empty() {
continue;
}
let item = serde_json::from_str(&line).map_err(|source| RuntimeError::StoreCorrupt {
operation,
path: path.to_path_buf(),
message: format!("line {}: {source}", index + 1),
})?;
items.push(item);
}
Ok(items)
}
fn atomic_write_json<T>(path: &Path, value: &T, operation: &'static str) -> Result<(), RuntimeError>
where
T: Serialize,
{
let parent = path.parent().ok_or_else(|| RuntimeError::StoreCorrupt {
operation,
path: path.to_path_buf(),
message: "path has no parent directory".to_string(),
})?;
fs::create_dir_all(parent).map_err(|source| RuntimeError::StoreIo {
operation,
path: parent.to_path_buf(),
source,
})?;
let tmp_path = tmp_path_for(path);
let write_result = (|| {
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&tmp_path)
.map_err(|source| RuntimeError::StoreIo {
operation,
path: tmp_path.clone(),
source,
})?;
serde_json::to_writer_pretty(&mut file, value).map_err(|source| {
RuntimeError::StoreCorrupt {
operation,
path: tmp_path.clone(),
message: format!("serialize json: {source}"),
}
})?;
file.write_all(b"\n")
.map_err(|source| RuntimeError::StoreIo {
operation,
path: tmp_path.clone(),
source,
})?;
file.sync_all().map_err(|source| RuntimeError::StoreIo {
operation,
path: tmp_path.clone(),
source,
})?;
drop(file);
fs::rename(&tmp_path, path).map_err(|source| RuntimeError::StoreIo {
operation,
path: path.to_path_buf(),
source,
})?;
sync_directory(parent, operation)
})();
if write_result.is_err() {
let _ = fs::remove_file(&tmp_path);
}
write_result
}
fn append_json_line<T>(path: &Path, value: &T, operation: &'static str) -> Result<(), RuntimeError>
where
T: Serialize,
{
let parent = path.parent().ok_or_else(|| RuntimeError::StoreCorrupt {
operation,
path: path.to_path_buf(),
message: "path has no parent directory".to_string(),
})?;
fs::create_dir_all(parent).map_err(|source| RuntimeError::StoreIo {
operation,
path: parent.to_path_buf(),
source,
})?;
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|source| RuntimeError::StoreIo {
operation,
path: path.to_path_buf(),
source,
})?;
serde_json::to_writer(&mut file, value).map_err(|source| RuntimeError::StoreCorrupt {
operation,
path: path.to_path_buf(),
message: format!("serialize json: {source}"),
})?;
file.write_all(b"\n")
.and_then(|()| file.flush())
.and_then(|()| file.sync_all())
.map_err(|source| RuntimeError::StoreIo {
operation,
path: path.to_path_buf(),
source,
})
}
fn ensure_file_exists(path: &Path, operation: &'static str) -> Result<(), RuntimeError> {
let parent = path.parent().ok_or_else(|| RuntimeError::StoreCorrupt {
operation,
path: path.to_path_buf(),
message: "path has no parent directory".to_string(),
})?;
fs::create_dir_all(parent).map_err(|source| RuntimeError::StoreIo {
operation,
path: parent.to_path_buf(),
source,
})?;
OpenOptions::new()
.create(true)
.append(true)
.open(path)
.and_then(|file| file.sync_all())
.map_err(|source| RuntimeError::StoreIo {
operation,
path: path.to_path_buf(),
source,
})
}
fn tmp_path_for(path: &Path) -> PathBuf {
let sequence = NEXT_TMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let file_name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("store");
path.with_file_name(format!(
".{file_name}.tmp-{}-{sequence}",
std::process::id()
))
}
fn sync_directory(path: &Path, operation: &'static str) -> Result<(), RuntimeError> {
File::open(path)
.and_then(|file| file.sync_all())
.map_err(|source| RuntimeError::StoreIo {
operation,
path: path.to_path_buf(),
source,
})
}

View File

@ -0,0 +1,82 @@
use serde::{Deserialize, Serialize};
use std::fmt;
/// Public Runtime identity.
///
/// This is the first half of Worker authority. Runtime APIs that operate on a
/// Worker require this id alongside a [`WorkerId`]; socket paths, session paths,
/// and display names are deliberately not authority.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct RuntimeId(String);
impl RuntimeId {
pub fn new(value: impl Into<String>) -> Option<Self> {
let value = value.into();
if value.trim().is_empty() {
None
} else {
Some(Self(value))
}
}
pub(crate) fn generated(sequence: u64) -> Self {
Self(format!("runtime-mem-{sequence:016x}"))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for RuntimeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// Runtime-local Worker identity.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct WorkerId(String);
impl WorkerId {
pub fn new(value: impl Into<String>) -> Option<Self> {
let value = value.into();
if value.trim().is_empty() {
None
} else {
Some(Self(value))
}
}
pub(crate) fn generated(sequence: u64) -> Self {
Self(format!("worker-{sequence:08x}"))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for WorkerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// Complete public authority reference for Worker operations.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct WorkerRef {
pub runtime_id: RuntimeId,
pub worker_id: WorkerId,
}
impl WorkerRef {
pub fn new(runtime_id: RuntimeId, worker_id: WorkerId) -> Self {
Self {
runtime_id,
worker_id,
}
}
}

View File

@ -0,0 +1,44 @@
use crate::catalog::WorkerStatus;
use crate::identity::WorkerRef;
use serde::{Deserialize, Serialize};
/// Input kind accepted by the embedded interaction API.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WorkerInputKind {
User,
System,
}
/// Worker input request. v0 stores the input in an in-memory transcript and
/// does not execute providers/tools.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerInput {
pub kind: WorkerInputKind,
pub content: String,
}
impl WorkerInput {
pub fn user(content: impl Into<String>) -> Self {
Self {
kind: WorkerInputKind::User,
content: content.into(),
}
}
pub fn system(content: impl Into<String>) -> Self {
Self {
kind: WorkerInputKind::System,
content: content.into(),
}
}
}
/// Acknowledgement returned after input is accepted into the in-memory Worker.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerInteractionAck {
pub worker_ref: WorkerRef,
pub status: WorkerStatus,
pub transcript_sequence: u64,
pub event_id: u64,
}

View File

@ -0,0 +1,22 @@
//! Embedded Runtime domain API for Worker management.
//!
//! `worker-runtime` intentionally stays independent from HTTP/WebSocket servers,
//! provider execution, and the existing Worker host. Filesystem persistence is
//! available only through the optional `fs-store` feature. The crate defines the
//! in-process Runtime authority surface that higher layers can later adapt into
//! registries or web APIs.
pub mod catalog;
pub mod diagnostics;
pub mod error;
#[cfg(feature = "fs-store")]
pub mod fs_store;
pub mod identity;
pub mod interaction;
pub mod management;
pub mod observation;
mod runtime;
#[cfg(feature = "fs-store")]
pub use fs_store::{FsRuntimeStore, FsRuntimeStoreOptions};
pub use runtime::Runtime;

View File

@ -0,0 +1,68 @@
use crate::identity::RuntimeId;
use serde::{Deserialize, Serialize};
/// Runtime backend kind.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeBackendKind {
Memory,
#[cfg(feature = "fs-store")]
FsStore,
}
/// Runtime lifecycle state.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeStatus {
Running,
Stopped,
}
/// Guardrails for bounded observation/projection APIs.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeLimits {
pub max_transcript_projection_items: usize,
pub max_event_batch_items: usize,
}
impl Default for RuntimeLimits {
fn default() -> Self {
Self {
max_transcript_projection_items: 256,
max_event_batch_items: 256,
}
}
}
/// Options used to construct an embedded memory Runtime.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeOptions {
pub runtime_id: Option<RuntimeId>,
pub display_name: Option<String>,
pub limits: RuntimeLimits,
}
impl Default for RuntimeOptions {
fn default() -> Self {
Self {
runtime_id: None,
display_name: None,
limits: RuntimeLimits::default(),
}
}
}
/// Management-plane summary for a Runtime.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeSummary {
pub runtime_id: RuntimeId,
pub display_name: Option<String>,
pub backend: RuntimeBackendKind,
pub status: RuntimeStatus,
pub worker_count: usize,
pub active_worker_count: usize,
pub stopped_worker_count: usize,
pub cancelled_worker_count: usize,
pub diagnostic_count: usize,
pub limits: RuntimeLimits,
}

View File

@ -0,0 +1,95 @@
use crate::identity::{RuntimeId, WorkerRef};
use serde::{Deserialize, Serialize};
/// Transcript role used by bounded projection.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TranscriptRole {
User,
System,
}
/// One projected transcript item.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TranscriptEntry {
pub sequence: u64,
pub worker_ref: WorkerRef,
pub role: TranscriptRole,
pub content: String,
pub event_id: u64,
}
/// Bounded transcript query.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TranscriptQuery {
pub start: usize,
pub limit: usize,
}
impl TranscriptQuery {
pub fn new(start: usize, limit: usize) -> Self {
Self { start, limit }
}
}
/// Bounded transcript projection response.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TranscriptProjection {
pub worker_ref: WorkerRef,
pub start: usize,
pub limit: usize,
pub total_items: usize,
pub items: Vec<TranscriptEntry>,
pub next_start: Option<usize>,
}
/// Event cursor. `next_event_id` is the first event id that should be returned
/// by the next poll.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EventCursor {
pub runtime_id: RuntimeId,
pub next_event_id: u64,
}
/// Placeholder subscription handle for future streaming APIs. v0 is explicit
/// poll-only so HTTP/WS/SSE dependencies are not pulled into this crate.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EventSubscription {
pub runtime_id: RuntimeId,
pub cursor: EventCursor,
pub mode: EventSubscriptionMode,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EventSubscriptionMode {
PollOnly,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeEvent {
pub id: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worker_ref: Option<WorkerRef>,
pub kind: RuntimeEventKind,
pub message: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RuntimeEventKind {
RuntimeStarted,
RuntimeStopped,
WorkerCreated,
WorkerInputAccepted,
WorkerStopped,
WorkerCancelled,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeEventBatch {
pub runtime_id: RuntimeId,
pub cursor: EventCursor,
pub events: Vec<RuntimeEvent>,
pub has_more: bool,
}

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter; filter = sourceFilter;
}; };
cargoHash = "sha256-+wsw/NKSCrouBhXgm4Mt5yk2gU87uTRYWwRSvJyiMLI="; cargoHash = "sha256-PFh+ZgmktkpeLRnIDLsxdT2QcA/j5rcJzkq7A9B6E44=";
depsExtraArgs = { depsExtraArgs = {
# Older fetchCargoVendor utilities used crates.io's API download endpoint, # Older fetchCargoVendor utilities used crates.io's API download endpoint,