Skip to content

Preserve state keys#1897

Open
McHersheys wants to merge 1 commit into
iii-hq:mainfrom
McHersheys:fix/state-key-labels-1896
Open

Preserve state keys#1897
McHersheys wants to merge 1 commit into
iii-hq:mainfrom
McHersheys:fix/state-key-labels-1896

Conversation

@McHersheys

@McHersheys McHersheys commented Jun 22, 2026

Copy link
Copy Markdown

Fixes #1896.

Tests:

  • corepack pnpm --filter console-frontend test -- src/api/state/state.test.ts

  • corepack pnpm --filter console-frontend lint

  • corepack pnpm --filter console-frontend build

  • I license my contributions to this repository under Apache 2.0, and I have all necessary rights over the code I am contributing.

Summary by CodeRabbit

  • Improvements
    • Enhanced state listing to normalize multiple response shapes and reliably extract keys/values, including clear placeholder keys when identifiers are missing.
    • State listing results now consistently return keyed entries (key + value) instead of value-only arrays.
  • New Features
    • Added a key/value index listing capability in the key-value store layer.
  • Tests
    • Expanded and updated normalization and listing tests to cover multiple formats and verify keyed output.

@vercel

vercel Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
iii-website Skipped Skipped Jun 22, 2026 7:16am

Request Review

@iii-hq-ci

iii-hq-ci Bot commented Jun 22, 2026

Copy link
Copy Markdown

License agreement recorded

@McHersheys, your agreement has been recorded and you have been added to contributors.md. All future PRs from your account will pass this check automatically.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

State list APIs now return keyed entries instead of value-only arrays across engine adapters, SDK interfaces, and console normalization. Console parsing accepts multiple response shapes and preserves semantic keys, with tests updated across the stack.

Changes

State list keyed entry flow

Layer / File(s) Summary
Contracts and public types
engine/src/workers/state/structs.rs, engine/src/workers/state/adapters/mod.rs, sdk/packages/node/iii-browser/src/state.ts, sdk/packages/node/iii/src/state.ts, sdk/packages/python/iii/src/iii/state.py, engine/src/workers/state/state.rs
Adds keyed list-item types and changes the state list contract to return keyed entries in the engine and SDK interfaces.
Engine adapters and state listing flow
engine/src/builtins/kv.rs, engine/src/workers/state/adapters/bridge.rs, engine/src/workers/state/adapters/kv_store.rs, engine/src/workers/state/adapters/redis_adapter.rs, engine/src/workers/state/trigger.rs, engine/src/workers/state/state.rs
Implements keyed entry output in the built-in KV store, bridge adapter, Redis adapter, and test stubs, and updates state::list metadata and assertions to match the new shape.
Console state normalization
console/packages/console-frontend/src/api/state/state.ts, console/packages/console-frontend/src/api/state/state.test.ts
Normalizes several /states/group payload shapes into StateItem[] with fallback keys and adds tests for explicit keys, entry keys, embedded state_key, and missing-key cases.
SDK contract updates and tests
sdk/packages/node/iii-example/src/state.ts, sdk/packages/node/iii/tests/state.test.ts, sdk/packages/python/iii/tests/test_state.py, sdk/packages/rust/iii/tests/state.rs
Updates example and test code to expect keyed state list items and compare returned key/value pairs explicitly.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • ytallo

Poem

🐇 The keys came out to hop and play,
No more item-N to block the way.
From engine vaults to console view,
The right old labels now shine through.
A key, a value, side by side —
The rabbit smiles with tidy pride.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: preserving semantic state keys in the states view.
Linked Issues check ✅ Passed The changes preserve keyed state entries across engine, SDK, and console normalization, matching the issue's required key preference and fallback behavior.
Out of Scope Changes check ✅ Passed The changes are focused on state-list key preservation and related tests, with no clear unrelated code paths added.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
console/packages/console-frontend/src/api/state/state.test.ts (1)

32-39: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a regression case for value.key beating a positional map key.

This test only proves plain map keys are used. It would not catch the /states regression where an object entry like item-0 masks an embedded semantic key.

Proposed test addition
   it('uses map keys for object-shaped item responses', async () => {
     mockStateItems({ items: { 'files/conscience/covenant.yaml': { raw: true } } })
 
     const { items } = await fetchStateItems('chambers')
 
     expect(items[0].key).toBe('files/conscience/covenant.yaml')
     expect(items[0].value).toEqual({ raw: true })
   })
+
+  it('uses explicit item keys before object entry keys', async () => {
+    mockStateItems({ items: { 'item-0': { key: 'worker', value: { status: 'ready' } } } })
+
+    const { items } = await fetchStateItems('chambers')
+
+    expect(items[0].key).toBe('worker')
+  })
🤖 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 `@console/packages/console-frontend/src/api/state/state.test.ts` around lines
32 - 39, Add a regression test case to the test file that verifies when a value
object contains its own `key` property, the actual map key is still correctly
used and not overridden by the embedded key. Extend or create a new test in the
state.test.ts file that mocks a response where an item value contains a `key`
property (for example, `{ raw: true, key: 'some-other-key' }`), and assert that
`items[0].key` resolves to the map key (the object property name) and not the
embedded `value.key` property. This regression test should prevent regressions
like the `/states` issue where an embedded `key` property could incorrectly mask
the positional map key.
🤖 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 `@console/packages/console-frontend/src/api/state/state.ts`:
- Around line 33-35: The stateItemValue function only unwraps item.value when
item.key or item.state_key are non-empty strings, but the code elsewhere accepts
item.id as a valid key source. Update the condition in the stateItemValue
function to also check nonEmptyString(item.id) alongside the existing checks for
item.key and item.state_key, so that the value is properly unwrapped regardless
of which key source is used.
- Around line 76-80: In the state.ts file within the map function that processes
rawItems, the priority order for selecting a key is incorrect. The code
currently uses "key || embeddedKey || fallbackKey(index)" which prioritizes the
Object.entries key before the embedded value.key property. Change the return
statement in makeStateItem to prioritize embeddedKey (which already checks
value.key first via nonEmptyString calls) over the map entry key, so the order
becomes "embeddedKey || key || fallbackKey(index)" instead. This ensures that an
explicit value.key property in the record takes precedence over the map entry
key before falling back to other alternatives.

---

Nitpick comments:
In `@console/packages/console-frontend/src/api/state/state.test.ts`:
- Around line 32-39: Add a regression test case to the test file that verifies
when a value object contains its own `key` property, the actual map key is still
correctly used and not overridden by the embedded key. Extend or create a new
test in the state.test.ts file that mocks a response where an item value
contains a `key` property (for example, `{ raw: true, key: 'some-other-key' }`),
and assert that `items[0].key` resolves to the map key (the object property
name) and not the embedded `value.key` property. This regression test should
prevent regressions like the `/states` issue where an embedded `key` property
could incorrectly mask the positional map key.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d38bc76f-66d8-4bf9-bd67-c3fdcb5b499e

📥 Commits

Reviewing files that changed from the base of the PR and between c4647cc and 95908ac.

📒 Files selected for processing (2)
  • console/packages/console-frontend/src/api/state/state.test.ts
  • console/packages/console-frontend/src/api/state/state.ts

Comment on lines +33 to +35
function stateItemValue(item: Record<string, unknown>): unknown {
if ('value' in item && (nonEmptyString(item.key) || nonEmptyString(item.state_key))) {
return item.value

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unwrap value consistently when id is used as the key.

Line 63 accepts item.id as a key source, but Line 34 only unwraps item.value for key/state_key. { id: 'worker', value: {...} } would render the wrapper object instead of the actual state value.

Proposed fix
 function stateItemValue(item: Record<string, unknown>): unknown {
-  if ('value' in item && (nonEmptyString(item.key) || nonEmptyString(item.state_key))) {
+  if (
+    'value' in item &&
+    (nonEmptyString(item.key) || nonEmptyString(item.state_key) || nonEmptyString(item.id))
+  ) {
     return item.value
   }
📝 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
function stateItemValue(item: Record<string, unknown>): unknown {
if ('value' in item && (nonEmptyString(item.key) || nonEmptyString(item.state_key))) {
return item.value
function stateItemValue(item: Record<string, unknown>): unknown {
if (
'value' in item &&
(nonEmptyString(item.key) || nonEmptyString(item.state_key) || nonEmptyString(item.id))
) {
return item.value
}
}
🤖 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 `@console/packages/console-frontend/src/api/state/state.ts` around lines 33 -
35, The stateItemValue function only unwraps item.value when item.key or
item.state_key are non-empty strings, but the code elsewhere accepts item.id as
a valid key source. Update the condition in the stateItemValue function to also
check nonEmptyString(item.id) alongside the existing checks for item.key and
item.state_key, so that the value is properly unwrapped regardless of which key
source is used.

Comment thread console/packages/console-frontend/src/api/state/state.ts Outdated
@McHersheys McHersheys force-pushed the fix/state-key-labels-1896 branch from 95908ac to fc62c41 Compare June 22, 2026 07:16
@anthonyiscoding

Copy link
Copy Markdown
Contributor

Thanks for the PR! We'll take a look and get back to you

@guibeira

Copy link
Copy Markdown
Contributor

Hey @McHersheys do you mind to update your branch? I tested you code and got this result:
This is the main:
image
This is from your branch:
image

The reason this is happening is that the engine is not returning the key. You can confirm this by running this:
iii trigger state::list scope=runtime | jq

Do you mind to check this, since the main issue was not addressed?

@McHersheys McHersheys force-pushed the fix/state-key-labels-1896 branch from fc62c41 to 5eb320a Compare June 24, 2026 14:37
@vercel

vercel Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

@McHersheys is attempting to deploy a commit to the motia Team on Vercel.

A member of the Team first needs to authorize it.

@McHersheys

McHersheys commented Jun 24, 2026

Copy link
Copy Markdown
Author

Thanks, you were right — the engine list path was still returning values without their state keys.

I updated the branch so state::list now returns key/value entries from the built-in, Redis, and bridge adapters, and updated the SDK/console types/tests to expect [{ key, value }].

Verified locally:

  • cargo check -p iii --lib
  • pnpm --filter console-frontend test -- src/api/state/state.test.ts
  • pnpm --filter console-frontend lint
  • pnpm --filter console-frontend build
  • pnpm --dir sdk/packages/node/iii build
  • pnpm --dir sdk/packages/node/iii-browser build
  • uv run --extra dev ruff check src/iii/state.py tests/test_state.py

One caveat: cargo test -p iii state --lib --no-default-features exceeded the 600s local command limit while compiling/linking the engine test binary, so I could not complete that Rust test run in this environment.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@engine/src/workers/state/adapters/bridge.rs`:
- Around line 89-98: In the state list conversion logic in bridge.rs, the
current unwrapping of result["items"] in the bridge adapter can incorrectly
treat a real state entry named "items" as an envelope. Update the result
handling in the state listing path (around the object-to-StateListItem mapping)
so that "items" is only unwrapped when the payload is clearly wrapped, and
otherwise preserve the original top-level key/value map unchanged. Keep the
existing StateListItem conversion behavior, but make the envelope detection
explicit before accessing the items field.

In `@sdk/packages/rust/iii/tests/state.rs`:
- Around line 233-244: The test in state::list is too lenient because it only
asserts arr.len() >= items.len() and then zips the sorted arrays, which can hide
extra unexpected rows; change the count check to require an exact match between
arr and items so the test fails on leaks or regressions. Keep the existing
sorting and entry-by-entry assertions in the same test scope, but make the
length assertion strict using the arr and items collections before the zip loop.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 34e4e936-5b22-4663-8a64-57b5e9a88a51

📥 Commits

Reviewing files that changed from the base of the PR and between fc62c41 and 5eb320a.

📒 Files selected for processing (17)
  • console/packages/console-frontend/src/api/state/state.test.ts
  • console/packages/console-frontend/src/api/state/state.ts
  • engine/src/builtins/kv.rs
  • engine/src/workers/state/adapters/bridge.rs
  • engine/src/workers/state/adapters/kv_store.rs
  • engine/src/workers/state/adapters/mod.rs
  • engine/src/workers/state/adapters/redis_adapter.rs
  • engine/src/workers/state/state.rs
  • engine/src/workers/state/structs.rs
  • engine/src/workers/state/trigger.rs
  • sdk/packages/node/iii-browser/src/state.ts
  • sdk/packages/node/iii-example/src/state.ts
  • sdk/packages/node/iii/src/state.ts
  • sdk/packages/node/iii/tests/state.test.ts
  • sdk/packages/python/iii/src/iii/state.py
  • sdk/packages/python/iii/tests/test_state.py
  • sdk/packages/rust/iii/tests/state.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • console/packages/console-frontend/src/api/state/state.test.ts
  • console/packages/console-frontend/src/api/state/state.ts

Comment on lines +89 to +98
let result = result.get("items").cloned().unwrap_or(result);

if let Some(object) = result.as_object() {
return Ok(object
.iter()
.map(|(key, value)| StateListItem {
key: key.clone(),
value: value.clone(),
})
.collect());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Handle top-level "items" without shadowing a real state key.

Line 89 unwraps result["items"] before checking whether the whole payload is already a plain key/value map. That drops or relabels legitimate state entries whose actual key is "items", which breaks the “preserve semantic keys” contract for arbitrary scopes. Please only treat items as an envelope when the response shape is unambiguously wrapped.

🤖 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 `@engine/src/workers/state/adapters/bridge.rs` around lines 89 - 98, In the
state list conversion logic in bridge.rs, the current unwrapping of
result["items"] in the bridge adapter can incorrectly treat a real state entry
named "items" as an envelope. Update the result handling in the state listing
path (around the object-to-StateListItem mapping) so that "items" is only
unwrapped when the payload is clearly wrapped, and otherwise preserve the
original top-level key/value map unchanged. Keep the existing StateListItem
conversion behavior, but make the envelope detection explicit before accessing
the items field.

Comment on lines 233 to +244
assert!(arr.len() >= items.len());

let mut result_sorted = arr.clone();
result_sorted.sort_by(|a, b| a["id"].as_str().cmp(&b["id"].as_str()));
result_sorted.sort_by(|a, b| a["key"].as_str().cmp(&b["key"].as_str()));

let mut items_sorted = items.clone();
items_sorted.sort_by(|a, b| a["id"].as_str().cmp(&b["id"].as_str()));

assert_eq!(result_sorted, items_sorted);
for (entry, item) in result_sorted.iter().zip(items_sorted.iter()) {
assert_eq!(entry["key"], item["id"]);
assert_eq!(entry["value"], *item);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restore an exact assertion for the returned entry count.

Line 233 only checks >=, and Lines 241-244 use zip, so extra unexpected rows are silently ignored. This scope is generated per test, so the assertion can stay strict and catch leaks/regressions in state::list.

Suggested tightening
-    assert!(arr.len() >= items.len());
+    assert_eq!(arr.len(), items.len());

     let mut result_sorted = arr.clone();
     result_sorted.sort_by(|a, b| a["key"].as_str().cmp(&b["key"].as_str()));

     let mut items_sorted = items.clone();
     items_sorted.sort_by(|a, b| a["id"].as_str().cmp(&b["id"].as_str()));
📝 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
assert!(arr.len() >= items.len());
let mut result_sorted = arr.clone();
result_sorted.sort_by(|a, b| a["id"].as_str().cmp(&b["id"].as_str()));
result_sorted.sort_by(|a, b| a["key"].as_str().cmp(&b["key"].as_str()));
let mut items_sorted = items.clone();
items_sorted.sort_by(|a, b| a["id"].as_str().cmp(&b["id"].as_str()));
assert_eq!(result_sorted, items_sorted);
for (entry, item) in result_sorted.iter().zip(items_sorted.iter()) {
assert_eq!(entry["key"], item["id"]);
assert_eq!(entry["value"], *item);
}
assert_eq!(arr.len(), items.len());
let mut result_sorted = arr.clone();
result_sorted.sort_by(|a, b| a["key"].as_str().cmp(&b["key"].as_str()));
let mut items_sorted = items.clone();
items_sorted.sort_by(|a, b| a["id"].as_str().cmp(&b["id"].as_str()));
for (entry, item) in result_sorted.iter().zip(items_sorted.iter()) {
assert_eq!(entry["key"], item["id"]);
assert_eq!(entry["value"], *item);
}
🤖 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 `@sdk/packages/rust/iii/tests/state.rs` around lines 233 - 244, The test in
state::list is too lenient because it only asserts arr.len() >= items.len() and
then zips the sorted arrays, which can hide extra unexpected rows; change the
count check to require an exact match between arr and items so the test fails on
leaks or regressions. Keep the existing sorting and entry-by-entry assertions in
the same test scope, but make the length assertion strict using the arr and
items collections before the zip loop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iii-console states view should preserve semantic state keys instead of item-N labels

3 participants