Skip to content

feat: state worker (builtin migration) - #436

Merged
guibeira merged 18 commits into
mainfrom
feat/state-worker
Jul 8, 2026
Merged

feat: state worker (builtin migration)#436
guibeira merged 18 commits into
mainfrom
feat/state-worker

Conversation

@guibeira

@guibeira guibeira commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds the standalone Rust state registry worker, replacing the built-in iii-state.
  • Registers the state trigger type and the six state::* functions (set, get, delete, update, list, list_groups) with exact id/input/output parity.
  • Ports the builtin's kv store (BuiltinKvStoreKvStore) with a byte-identical on-disk format (rkyv .bin per scope), plus the redis adapter with the same state:<scope> hash layout and atomic Lua update script.
  • Event fan-out with builtin parity: state:created/state:updated/state:deleted, scope/key pre-filter, condition functions (only explicit false blocks), triggers_enabled live gate.
  • Three-tier configuration hot-reload via the configuration worker: live (triggers_enabled, max_value_bytes), task-rebuild (save_interval_ms save-loop respawn), restart-tier (adapter).
  • Boot guard: refuses to start while the built-in iii-state is active, with a store-migration hint.
  • Adds manifest output, CI/release wiring (create-tag.yml + release.yml), README parity documentation, registry skill documentation, and connect-or-skip e2e coverage.

Behavior

  • Keeps the state trigger type and all six state::* function ids with identical input/output shapes.
  • On-disk kv format is byte-identical to the builtin (rkyv) — an existing state_store.db loads unchanged.
  • Latency parity benchmark waived by project decision (2026-07-06); the README documents the in-process µs → WS round-trip ms delta instead.
  • Documented parity deltas (README table): error codes become message prefixes (SDK handlers carry a message, not a coded body); trigger metadata not forwarded (no field on iii-sdk 0.20 TriggerRequest, same limitation as the http worker); bridge adapter not ported (ADR 0001); telemetry counters out of scope.
  • Refuses to boot if the built-in iii-state worker is still connected.
  • Crate is edition 2024 (http/cron are 2021): required so the verbatim engine ports (let-chains in update_ops.rs) compile unchanged.

Validation

  • cd state && cargo test — 96 unit tests green (includes the verbatim-ported update_ops suite and the builtin-format kv-store compat test)
  • III_E2E_REQUIRE=1 cargo test --test e2e_state against a live engine (workers: []) — 8/8 e2e cases pass (ops/trigger/config parity, hot-reload)
  • cargo test --test e2e_state without an engine — connect-or-skip, green
  • cd state && cargo fmt --check
  • cd state && cargo clippy --all-targets -- -D warnings
  • cargo doc --no-deps — zero warnings
  • cd state && cargo build && ./target/debug/state --manifest | head -5
  • python3 .github/scripts/discover_changed_workers.py --base main — state lands in the rust bucket
  • python3 .github/scripts/validate_worker.py --worker state --base-ref main --source-changed '["state"]' — exit 0, no hard failures
  • python3 .github/scripts/build_skills_payload.py --worker state --version 0.1.0 — collects the skill

Notes

  • PR is draft while the migration stack settles.
  • Release tag state/v0.1.0 is post-merge only, via the Create Tag workflow (this PR wires state into create-tag.yml and 'state/v*' into release.yml).
  • ADR 0001 (docs/adr/0001-state-worker-store-location.md) records the store-location decision (store lives in the worker process) and the benchmark waiver.
  • Local Python smoke suite (test_python_state/) not yet run — pending before flipping the PR to ready.

Summary by CodeRabbit

  • New Features

    • Added a standalone state worker with key/value storage, trigger events, config reloads, and update operations.
    • Added support for new state-related tags and workflow dispatch worker selection.
  • Documentation

    • Added architecture and usage docs for the state worker, including configuration and migration guidance.
  • Tests

    • Added end-to-end coverage for state storage, triggers, updates, and live configuration changes.

guibeira added 17 commits July 6, 2026 16:33
…nd/remove parity)

Bumps the crate to edition 2024: the engine source uses let-chains; keeping the port byte-verbatim outweighs matching http/cron's edition 2021.
…d comments

- README parity table: note that trigger metadata is not forwarded to
  handlers (iii-sdk 0.20 TriggerRequest has no metadata field; same
  limitation as the http worker).
- condition.rs: reword module doc from the http-worker's per-route
  middleware framing to state's actual context (condition gates
  change-event fan-out per trigger binding), and point the test
  comment at the real e2e coverage (condition_false_blocks_null_passes
  in tests/e2e_state.rs) instead of a nonexistent e2e_condition suite.
- functions.rs / configuration.rs: backtick doc references to
  `#[function]` and to private items (StateCtx::snapshot,
  on_config_change) instead of broken intra-doc links.
…ltin

Ports test_reconfigure_reverts_to_boot_interval_when_cleared,
test_boot_interval_is_floored, test_reconfigure_in_memory_is_noop, and
test_kv_store_invalid_store_method (renamed from
test_builtin_kv_store_invalid_store_method) from the engine's built-in
kv.rs test suite; KvStore's save-loop/lock surface is unchanged from
the builtin so all four ported as-is.
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jul 8, 2026 12:15pm
workers-tech-spec Ready Ready Preview, Comment Jul 8, 2026 12:15pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds a new standalone Rust "state" worker (iii-state) implementing a pluggable key-value store with kv (in-memory/file-backed) and Redis adapters, state::* functions, trigger fan-out with conditional gating, live configuration reload, boot/shutdown lifecycle, extensive unit/e2e tests, documentation, and CI workflow updates.

Changes

State Worker

Layer / File(s) Summary
CI and crate scaffolding
.github/workflows/create-tag.yml, .github/workflows/release.yml, state/Cargo.toml, state/build.rs, state/iii.worker.yaml, state/src/lib.rs
Adds state worker option/tag trigger to CI, and scaffolds the iii-state crate manifest, build script, worker manifest, and module exports.
Config, structs, and manifest contracts
state/src/config.rs, state/src/structs.rs, state/src/manifest.rs
Defines StateConfig/adapter config types with JSON schema, shared request/event structs, and the module manifest builder.
File-backed KV store
state/src/store.rs
Implements KvStore with in-memory/file persistence, background save loop, and reconfiguration.
Update operations engine
state/src/update_ops.rs
Implements apply_update_ops for Set/Merge/Increment/Decrement/Append/Remove with validation and prototype-pollution protection.
Adapters (KV/Redis)
state/src/adapters.rs
Defines StateAdapter trait, KvStoreAdapter, RedisAdapter (Lua-scripted atomic ops), and build_adapter selection.
Triggers, conditions, and event fan-out
state/src/trigger.rs, state/src/condition.rs, state/src/events.rs
Implements trigger registration/matching, conditional gating, and async trigger fan-out.
state::* functions
state/src/functions.rs
Registers state::set/get/delete/update/list/list_groups with size limits and event emission.
Live configuration reload
state/src/configuration.rs
Registers config schema, fetches live config, and applies tiered hot-reload behavior.
Boot sequence and CLI entrypoint
state/src/boot.rs, state/src/main.rs
Implements BootHandle/start with built-in worker guard, and the binary entrypoint with CLI, registration, and shutdown.
Documentation
docs/adr/0001-state-worker-store-location.md, state/README.md, state/skills/SKILL.md
Adds ADR, README, and skill documentation for the standalone state worker.
Test harness and e2e scenarios
state/tests/common/*, state/tests/e2e_state.rs
Adds engine connection helpers and e2e tests covering CRUD, triggers, conditions, and config reload.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant StateFunctions as "state::* functions"
  participant StateAdapter
  participant TriggerTable
  participant Invoker as "SdkInvoker"
  participant ConditionFn as "condition function"

  Caller->>StateFunctions: state::set(scope, key, value)
  StateFunctions->>StateAdapter: set(scope, key, value)
  StateAdapter-->>StateFunctions: old_value, new_value
  StateFunctions->>TriggerTable: fan_out(event: Created/Updated)
  TriggerTable->>TriggerTable: match bindings by scope/key
  TriggerTable->>ConditionFn: check_condition(payload)
  ConditionFn-->>TriggerTable: true/false/null
  TriggerTable->>Invoker: call(function_id, event payload)
  Invoker-->>TriggerTable: handler result/error
  StateFunctions-->>Caller: StreamSetResult
Loading
sequenceDiagram
  participant Main as "main.rs"
  participant IIIClient
  participant Boot as "boot::start"
  participant Configuration as "configuration.rs"
  participant Adapter as "StateAdapter"

  Main->>IIIClient: register_worker
  Main->>Configuration: register_config(seed)
  Main->>Configuration: fetch_config
  Main->>Boot: start(iii, config)
  Boot->>Boot: guard_against_builtin_state
  Boot->>Adapter: build_adapter(config)
  Boot->>Boot: register state trigger + functions
  Boot-->>Main: BootHandle
  Main->>Configuration: register_config_trigger(ctx, apply_lock)
  Configuration-->>Boot: on configuration:updated, reconfigure adapter
  Main->>Boot: shutdown()
  Boot->>Adapter: destroy()
Loading

Poem

A burrow of state, keys tucked in a row,
With Redis and files, wherever they go. 🐇
Triggers hop softly on scope and on key,
Conditions say "false" — or let them run free.
Config reloads live, no restart, no fuss —
This rabbit's proud burrow now ships with us!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding the standalone state worker as a builtin migration.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/state-worker

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 40 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

The registry publish gate (collect_worker_interface.py --assert-typed-schemas)
rejects state::get, state::delete, and state::list: their handlers return
Option<serde_json::Value> (the raw stored value, or null), which schemars
auto-extracts to the permissive Nullable_AnyValue schema — no type/properties
keyword, flagged as "unknown" by the gate.

Override response_format with an explicit typed "JSON value or null" schema
(type: [string, number, boolean, object, array, null]) for the three
handlers. This only annotates the registered schema; the handlers still
return the exact same Option<Value> on the wire.

Verified by reproducing the CI collection + assert locally against a live
engine + worker: worker-interface.json now passes
--assert-non-empty --assert-typed-schemas for all 7 state::* functions.
96 unit tests + 8 e2e tests still green.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
state/src/configuration.rs (1)

89-102: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid retrying a definitive NOT_FOUND through trigger_with_retry.

try_get_config_value routes configuration::get through trigger_with_retry, which retries every error uniformly. A NOT_FOUND (the expected first-boot state) is therefore retried CONFIG_RETRIES times with cumulative backoff before being recognized here. Since both should_seed_initial_value and fetch_config call this on boot, first-run startup pays this wait twice for a result that is already definitive. Consider short-circuiting NOT_FOUND before the retry loop.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@state/src/configuration.rs` around lines 89 - 102, `try_get_config_value` is
retrying a definitive `NOT_FOUND` from `trigger_with_retry`, which causes
unnecessary boot delays in both `should_seed_initial_value` and `fetch_config`.
Update the `configuration::get` path so `NOT_FOUND` is detected before entering
the retry loop, and return `Ok(None)` immediately instead of letting
`trigger_with_retry` apply `CONFIG_RETRIES` and backoff. Keep the existing
`trigger_with_retry` behavior for other errors, and preserve the
`try_get_config_value` return shape.
state/src/store.rs (1)

178-209: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider cloning config once instead of repeatedly.

config.clone() is called three times (lines 181, 195, 204) to extract store_method, file_path, and save_interval_ms. While serde_json::Value clones are Arc-cheap, a single clone at the top would be cleaner.

♻️ Optional refactor
 pub fn new(config: Option<Value>) -> Self {
     tracing::debug!("Initializing KvStore with config: {:?}", config);
-    let store_method = config
-        .clone()
-        .and_then(|cfg| {
-            cfg.get("store_method")
-                .and_then(|v| v.as_str())
-                .map(|s| s.to_string())
-        })
-        .unwrap_or_else(|| "in_memory".to_string());
+    let cfg = config.as_ref();
+    let store_method = cfg
+        .and_then(|c| c.get("store_method").and_then(|v| v.as_str()))
+        .unwrap_or("in_memory")
+        .to_string();

-    if store_method == "in_memory" {
+    if store_method == "in_memory" {
         tracing::warn!(
             "DO NOT USE IN_MEMORY STORE_METHOD IN PRODUCTION - DATA WILL BE LOST ON SHUTDOWN"
         );
     }

-    let file_path = config
-        .clone()
-        .and_then(|cfg| {
-            cfg.get("file_path")
-                .and_then(|v| v.as_str())
-                .map(|s| s.to_string())
-        })
-        .unwrap_or_else(|| "kv_store_data.db".to_string());
+    let file_path = cfg
+        .and_then(|c| c.get("file_path").and_then(|v| v.as_str()))
+        .unwrap_or("kv_store_data.db")
+        .to_string();

-    let interval = config
-        .clone()
-        .and_then(|cfg| cfg.get("save_interval_ms").and_then(|v| v.as_u64()))
+    let interval = cfg
+        .and_then(|c| c.get("save_interval_ms").and_then(|v| v.as_u64()))
         .filter(|&n| n > 0)
         .unwrap_or(DEFAULT_SAVE_INTERVAL_MS)
         .max(MIN_SAVE_INTERVAL_MS);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@state/src/store.rs` around lines 178 - 209, Clone config once at the start of
KvStore::new and reuse that cloned value to derive store_method, file_path, and
save_interval_ms instead of calling config.clone() multiple times. Keep the
existing extraction logic inside new, but switch each lookup to read from the
single cloned config so the constructor is cleaner and easier to maintain.
state/src/events.rs (1)

25-38: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add timeout to SdkInvoker::call

timeout_ms: None means a hanging trigger handler blocks the spawned fan_out task indefinitely, leaking resources. check_condition in condition.rs already accepts a timeout_ms parameter — pass a configurable timeout here too for consistency.

♻️ Proposed fix
 pub struct SdkInvoker {
     pub iii: Arc<IIIClient>,
+    pub timeout_ms: u64,
 }

 #[async_trait::async_trait]
 impl Invoker for SdkInvoker {
     async fn call(&self, function_id: &str, payload: Value) -> Result<Value, String> {
         self.iii
             .trigger(TriggerRequest {
                 function_id: function_id.to_string(),
                 payload,
                 action: None,
-                timeout_ms: None,
+                timeout_ms: Some(self.timeout_ms),
             })
             .await
             .map_err(|e| e.to_string())
     }
 }

The timeout_ms value can be sourced from StateConfig at construction time, similar to how check_condition receives it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@state/src/events.rs` around lines 25 - 38, Add a configurable timeout to
SdkInvoker::call so TriggerRequest is not created with timeout_ms: None. Thread
the timeout in from StateConfig at construction time for SdkInvoker, and pass it
through in the trigger call the same way check_condition in condition.rs already
uses a timeout_ms value. Update the SdkInvoker constructor/fields and the call
method to use that stored timeout consistently.
state/src/adapters.rs (1)

587-607: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Remove the Mutex around ConnectionManager ConnectionManager already supports shared concurrent use, so the lock just serializes Redis operations and adds avoidable contention. Store it directly and clone it where each method needs a handle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@state/src/adapters.rs` around lines 587 - 607, The RedisAdapter::new
constructor currently wraps ConnectionManager in Arc<Mutex<_>>, but
ConnectionManager is already safe for shared concurrent use and the mutex adds
unnecessary contention. Update RedisAdapter to store the ConnectionManager
directly (or inside an Arc without Mutex if sharing is needed) and adjust the
RedisAdapter field and any methods that currently lock publisher to clone and
use the connection manager handle instead of serializing access.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@state/src/main.rs`:
- Around line 100-108: The post-boot path in start() can exit early on
register_config_trigger() failure without cleaning up the started boot state.
Wrap the booted section so boot.shutdown() is always called on any error path
after iii_state::boot::start(), including the register_config_trigger() call,
and ensure iii.shutdown_async() still runs during overall teardown; use the
existing boot and register_config_trigger symbols to keep the cleanup localized.

---

Nitpick comments:
In `@state/src/adapters.rs`:
- Around line 587-607: The RedisAdapter::new constructor currently wraps
ConnectionManager in Arc<Mutex<_>>, but ConnectionManager is already safe for
shared concurrent use and the mutex adds unnecessary contention. Update
RedisAdapter to store the ConnectionManager directly (or inside an Arc without
Mutex if sharing is needed) and adjust the RedisAdapter field and any methods
that currently lock publisher to clone and use the connection manager handle
instead of serializing access.

In `@state/src/configuration.rs`:
- Around line 89-102: `try_get_config_value` is retrying a definitive
`NOT_FOUND` from `trigger_with_retry`, which causes unnecessary boot delays in
both `should_seed_initial_value` and `fetch_config`. Update the
`configuration::get` path so `NOT_FOUND` is detected before entering the retry
loop, and return `Ok(None)` immediately instead of letting `trigger_with_retry`
apply `CONFIG_RETRIES` and backoff. Keep the existing `trigger_with_retry`
behavior for other errors, and preserve the `try_get_config_value` return shape.

In `@state/src/events.rs`:
- Around line 25-38: Add a configurable timeout to SdkInvoker::call so
TriggerRequest is not created with timeout_ms: None. Thread the timeout in from
StateConfig at construction time for SdkInvoker, and pass it through in the
trigger call the same way check_condition in condition.rs already uses a
timeout_ms value. Update the SdkInvoker constructor/fields and the call method
to use that stored timeout consistently.

In `@state/src/store.rs`:
- Around line 178-209: Clone config once at the start of KvStore::new and reuse
that cloned value to derive store_method, file_path, and save_interval_ms
instead of calling config.clone() multiple times. Keep the existing extraction
logic inside new, but switch each lookup to read from the single cloned config
so the constructor is cleaner and easier to maintain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fa472aef-0ecb-4681-b484-ca0e01049313

📥 Commits

Reviewing files that changed from the base of the PR and between 3a826b5 and a88d78a.

⛔ Files ignored due to path filters (1)
  • state/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • .github/workflows/create-tag.yml
  • .github/workflows/release.yml
  • docs/adr/0001-state-worker-store-location.md
  • state/Cargo.toml
  • state/README.md
  • state/build.rs
  • state/iii.worker.yaml
  • state/skills/SKILL.md
  • state/src/adapters.rs
  • state/src/boot.rs
  • state/src/condition.rs
  • state/src/config.rs
  • state/src/configuration.rs
  • state/src/events.rs
  • state/src/functions.rs
  • state/src/lib.rs
  • state/src/main.rs
  • state/src/manifest.rs
  • state/src/store.rs
  • state/src/structs.rs
  • state/src/trigger.rs
  • state/src/update_ops.rs
  • state/tests/common/engine.rs
  • state/tests/common/mod.rs
  • state/tests/e2e_state.rs

Comment thread state/src/main.rs
Comment on lines +100 to +108
let boot = iii_state::boot::start(iii.clone(), config.clone()).await?;
tracing::info!(adapter = %config.effective_adapter_name(), "iii-state ready");

// Subscribe to configuration:updated so triggers_enabled/max_value_bytes
// reload live and a save_interval_ms change respawns the adapter's save
// loop (see configuration).
configuration::register_config_trigger(&iii, boot.ctx.clone(), boot.apply_lock.clone())
.map_err(anyhow::Error::msg)
.context("binding configuration trigger")?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Call boot.shutdown() on the register_config_trigger error path.

If register_config_trigger fails, the ? operator returns early without calling boot.shutdown() or iii.shutdown_async(). The kv adapter's save loop may have pending dirty entries that won't flush, and the engine won't receive a clean disconnect. Wrap the post-boot logic so shutdown always runs.

🛡️ Proposed fix: guarantee cleanup on all post-boot paths
 let boot = iii_state::boot::start(iii.clone(), config.clone()).await?;
 tracing::info!(adapter = %config.effective_adapter_name(), "iii-state ready");

-// Subscribe to configuration:updated so triggers_enabled/max_value_bytes
-// reload live and a save_interval_ms change respawns the adapter's save
-// loop (see configuration).
-configuration::register_config_trigger(&iii, boot.ctx.clone(), boot.apply_lock.clone())
-    .map_err(anyhow::Error::msg)
-    .context("binding configuration trigger")?;
-
-tokio::signal::ctrl_c().await?;
-tracing::info!("iii-state shutting down");
-boot.shutdown().await;
-iii.shutdown_async().await;
-Ok(())
+let result = async {
+    // Subscribe to configuration:updated so triggers_enabled/max_value_bytes
+    // reload live and a save_interval_ms change respawns the adapter's save
+    // loop (see configuration).
+    configuration::register_config_trigger(&iii, boot.ctx.clone(), boot.apply_lock.clone())
+        .map_err(anyhow::Error::msg)
+        .context("binding configuration trigger")?;
+
+    tokio::signal::ctrl_c().await?;
+    tracing::info!("iii-state shutting down");
+    Ok::<(), anyhow::Error>(())
+}
+.await;
+
+boot.shutdown().await;
+iii.shutdown_async().await;
+result
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let boot = iii_state::boot::start(iii.clone(), config.clone()).await?;
tracing::info!(adapter = %config.effective_adapter_name(), "iii-state ready");
// Subscribe to configuration:updated so triggers_enabled/max_value_bytes
// reload live and a save_interval_ms change respawns the adapter's save
// loop (see configuration).
configuration::register_config_trigger(&iii, boot.ctx.clone(), boot.apply_lock.clone())
.map_err(anyhow::Error::msg)
.context("binding configuration trigger")?;
let boot = iii_state::boot::start(iii.clone(), config.clone()).await?;
tracing::info!(adapter = %config.effective_adapter_name(), "iii-state ready");
let result = async {
// Subscribe to configuration:updated so triggers_enabled/max_value_bytes
// reload live and a save_interval_ms change respawns the adapter's save
// loop (see configuration).
configuration::register_config_trigger(&iii, boot.ctx.clone(), boot.apply_lock.clone())
.map_err(anyhow::Error::msg)
.context("binding configuration trigger")?;
tokio::signal::ctrl_c().await?;
tracing::info!("iii-state shutting down");
Ok::<(), anyhow::Error>(())
}
.await;
boot.shutdown().await;
iii.shutdown_async().await;
result
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@state/src/main.rs` around lines 100 - 108, The post-boot path in start() can
exit early on register_config_trigger() failure without cleaning up the started
boot state. Wrap the booted section so boot.shutdown() is always called on any
error path after iii_state::boot::start(), including the
register_config_trigger() call, and ensure iii.shutdown_async() still runs
during overall teardown; use the existing boot and register_config_trigger
symbols to keep the cleanup localized.

@guibeira
guibeira merged commit c48ddfe into main Jul 8, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant