Skip to content

feat(OMN-2117): Canonical state nodes — session state effect + lifecycle FSM reducer - #300

Merged
jonahgabriel merged 23 commits into
mainfrom
jonah/omn-2117-omnibase_infra-p1-canonical-state-nodes-session-state-effect
Feb 11, 2026
Merged

jonahgabriel merged 23 commits into
mainfrom
jonah/omn-2117-omnibase_infra-p1-canonical-state-nodes-session-state-effect

Conversation

@jonahgabriel

@jonahgabriel jonahgabriel commented Feb 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Implement node_session_state_effect (EFFECT node) with 5 handlers for filesystem I/O on ~/.claude/state/
  • Implement node_session_lifecycle_reducer (REDUCER node) with pure FSM: idle → run_created → run_active → run_ended → idle
  • Add EnumSessionLifecycleState enum and 4 Pydantic models: ModelSessionIndex, ModelRunContext, ModelSessionLifecycleState, ModelSessionStateResult
  • 49 new unit tests covering all handlers, models, FSM transitions, concurrent pipeline isolation, and stale run GC

Key Design Decisions

  • Atomic writes: write-tmp → fsync → rename (POSIX guarantees)
  • Concurrent safety: flock on session.json, no lock needed for per-run documents (single-writer)
  • Stale run GC: 4hr TTL, also cleans malformed files
  • run_id is str, not UUID: Used in filesystem paths (runs/{run_id}.json) and for human readability

Test plan

  • 49 new unit tests all passing
  • Full unit test suite (11,068 tests) passing
  • All 18 pre-commit hooks passing (including ONEX pattern, naming, architecture, union, any-type, circular import, declarative node validators)
  • ruff check clean
  • mypy clean

Closes OMN-2117

Summary by CodeRabbit

  • New Features

    • Session lifecycle FSM, enum and immutable models for idle → created → active → ended; session-state effect node, registries and factories to wire nodes.
  • Reliability

    • Atomic, crash‑safe, concurrency‑safe read/write with path‑validation; configurable GC for stale runs; validation threshold raised.
  • Tests

    • Extensive unit tests for models, handlers, lifecycle transitions, concurrency and end‑to‑end scenarios.
  • Chores

    • Validation exemptions added for run_id fields.

…cle FSM reducer

Implement two new canonical nodes for concurrent pipeline session state management:

**node_session_state_effect (EFFECT)**:
- 5 handlers: SessionIndexRead, SessionIndexWrite, RunContextRead, RunContextWrite, StaleRunGC
- Atomic writes via write-tmp-fsync-rename pattern
- flock-based locking for session.json (concurrent pipeline safety)
- Run context documents are single-writer (no lock needed)
- Stale run GC with configurable TTL (default 4hr)

**node_session_lifecycle_reducer (REDUCER)**:
- Pure FSM: idle → run_created → run_active → run_ended → idle
- Immutable state transitions via with_* methods
- State guard methods (can_create_run, can_activate_run, etc.)
- Idempotent event replay via last_processed_event_id tracking

Models: ModelSessionIndex (session.json), ModelRunContext (runs/{run_id}.json),
ModelSessionLifecycleState (FSM state), ModelSessionStateResult (operation result)

49 new tests covering: model validation, handler I/O, concurrent pipeline
isolation, stale run GC, full FSM lifecycle, idempotent replay
@coderabbitai

coderabbitai Bot commented Feb 11, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a session lifecycle enum and reducer plus a contract-driven session-state filesystem effect node: new immutable models, handlers for atomic I/O, locking and GC, DI registries, contracts, validation exemptions and INFRA_MAX_UNIONS bump, with extensive unit tests.

Changes

Cohort / File(s) Summary
Enums
src/omnibase_infra/enums/enum_session_lifecycle_state.py, src/omnibase_infra/enums/__init__.py
Add EnumSessionLifecycleState and export it from the enums package.
Session Lifecycle Reducer
src/omnibase_infra/nodes/node_session_lifecycle_reducer/...
src/omnibase_infra/nodes/node_session_lifecycle_reducer/node.py, .../models/*, .../registry/*, .../contract.yaml, .../__init__.py
New reducer package: ModelSessionLifecycleState (immutable FSM), NodeSessionLifecycleReducer, registry, and declarative contract describing states, transitions, and intent emissions.
Session State Effect Node & Registry
src/omnibase_infra/nodes/node_session_state_effect/node.py, .../registry/*, .../__init__.py, .../contract.yaml
New effect node for filesystem session state (~/.claude/state/), registry factory, and contract metadata.
Session State Handlers
src/omnibase_infra/nodes/node_session_state_effect/handlers/*
Add handlers: HandlerSessionIndexRead/Write, HandlerRunContextRead/Write, HandlerStaleRunGC — atomic write patterns, flock locking (POSIX), path-traversal defenses, parsing/validation, and GC logic.
Session State Models
src/omnibase_infra/nodes/node_session_state_effect/models/*
Add ModelRunContext, ModelSessionIndex, ModelSessionStateResult, RUN_ID_PATTERN, validate_run_id — frozen Pydantic models with validators, pure-transition helpers, and staleness checks.
Package Exports
src/omnibase_infra/nodes/__init__.py, various __init__.py files
Re-export new enums, models, reducers, effects, handlers, and registries at package level for public API surface.
Validation updates
src/omnibase_infra/validation/infra_validators.py, src/omnibase_infra/validation/validation_exemptions.yaml
Bump INFRA_MAX_UNIONS 119→121 and add validation exemptions for run_id fields (explicitly non-UUID human-readable IDs).
Tests
tests/unit/nodes/node_session_lifecycle_reducer/*, tests/unit/nodes/node_session_state_effect/*, tests/unit/validation/*
Extensive unit tests covering FSM transitions, model invariants, handler I/O behavior (atomicity, concurrency), path-traversal defenses, stale-run GC, and updated validator expectations.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Pipeline Caller
    participant Reducer as NodeSessionLifecycleReducer
    participant State as ModelSessionLifecycleState

    Caller->>Reducer: create_run(run_id, event_id)
    Reducer->>State: can_create_run()
    State-->>Reducer: allowed/denied
    alt allowed
        Reducer->>State: with_run_created(run_id, event_id)
        State-->>Reducer: new state RUN_CREATED
        Reducer-->>Caller: updated state
    else denied
        Reducer-->>Caller: error
    end

    Caller->>Reducer: activate_run(event_id)
    Reducer->>State: can_activate_run()
    State-->>Reducer: allowed/denied
    alt allowed
        Reducer->>State: with_run_activated(event_id)
        State-->>Reducer: new state RUN_ACTIVE
        Reducer-->>Caller: updated state
    else denied
        Reducer-->>Caller: error
    end
Loading
sequenceDiagram
    participant Caller as Pipeline Caller
    participant Effect as NodeSessionStateEffect
    participant Handler as HandlerSessionIndexRead
    participant FS as Filesystem (~/.claude/state/)

    Caller->>Effect: session.index.read(correlation_id)
    Effect->>Handler: handle(correlation_id)
    Handler->>FS: stat/read session.json
    alt file exists
        FS-->>Handler: file contents
        Handler->>Handler: parse & validate -> ModelSessionIndex
        Handler-->>Effect: ModelSessionIndex + success
    else missing
        Handler-->>Effect: default ModelSessionIndex + success
    end
    Effect-->>Caller: ModelSessionStateResult
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐇 I hopped through enums, state, and file,

I fenced each path and wrote with temp-and-while.
I locked and fsync'd so nothing would fall,
GC swept the crumbs and answered the call.
Hooray — FSM and state now snug in my hall!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the two main additions: session state effect node and lifecycle FSM reducer, with explicit reference to the ticket OMN-2117.
Docstring Coverage ✅ Passed Docstring coverage is 96.12% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jonah/omn-2117-omnibase_infra-p1-canonical-state-nodes-session-state-effect

No actionable comments were generated in the recent review. 🎉


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

@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: 10

🤖 Fix all issues with AI agents
In
`@src/omnibase_infra/nodes/node_session_state_effect/handlers/handler_run_context_read.py`:
- Around line 24-34: The HandlerRunContextRead class is missing the required
handler_type and handler_category properties; import EnumHandlerType and
EnumHandlerTypeCategory and add two `@property` methods on HandlerRunContextRead
named handler_type and handler_category returning the appropriate enums (follow
the same pattern as other handlers such as handler_ledger_projection.py and
handler_postgres_cleanup_topics.py); typically implement handler_type to return
EnumHandlerType.READ and handler_category to return the corresponding category
used for session/state handlers (e.g., EnumHandlerTypeCategory.STATE or the
category used by other read handlers in the codebase).

In
`@src/omnibase_infra/nodes/node_session_state_effect/handlers/handler_run_context_write.py`:
- Around line 27-32: The class HandlerRunContextWrite is missing the mandatory
handler_type and handler_category properties required by the handler contract;
add two `@property` methods on HandlerRunContextWrite named handler_type(self) ->
EnumHandlerType and handler_category(self) -> EnumHandlerTypeCategory that
return the appropriate EnumHandlerType and EnumHandlerTypeCategory values for
this handler (use the enum members that represent run-context write behavior in
your codebase), so registration/routing can discover this handler.
- Around line 94-101: In the OSError except block in handler_run_context_write,
don't embed raw exception text into the returned ModelSessionStateResult.error
or logs; instead import and call the sanitization utility from
omnibase_infra.utils.util_error_sanitization (e.g., sanitize_error_message) on
str(e) and use the sanitized string when populating
ModelSessionStateResult(error=...) and the logger.warning message (keep
context.run_id and correlation_id as before). Ensure you replace direct uses of
e with the sanitized message so no filesystem paths or sensitive data are
returned or logged.
- Around line 56-69: The code uses context.run_id directly to build filesystem
names (run_path and the tempfile prefix in handler_run_context_write), which
allows path traversal; validate or sanitize run_id before using it by enforcing
a strict safe pattern (e.g., UUID or slug regex like ^[A-Za-z0-9_-]+$) or
reject/raise if it fails, and only then construct run_path = runs_dir /
f"{safe_run_id}.json" and use safe_run_id in tempfile.mkstemp prefix;
alternatively, if you must accept arbitrary IDs, hex-encode or base64-url-safe
encode context.run_id into a filesystem-safe token and use that token for both
run_path and tmp file prefixes (apply this check/sanitation in the handler where
run_path and tempfile.mkstemp are created).

In
`@src/omnibase_infra/nodes/node_session_state_effect/handlers/handler_session_index_read.py`:
- Around line 24-29: Add the required handler properties to the
HandlerSessionIndexRead class: implement a handler_type property that returns
EnumHandlerType.READ and a handler_category property that returns
EnumHandlerTypeCategory.STATE (import both EnumHandlerType and
EnumHandlerTypeCategory if not already imported); ensure the properties are
simple `@property` methods on HandlerSessionIndexRead so the handler contract can
register/route the class correctly.
- Around line 80-102: The exception handlers in handler_session_index_read.py
currently embed raw exception text (variable e) into logger messages and the
ModelSessionStateResult.error field; sanitize these strings by calling
omnibase_infra.utils.util_error_sanitization on str(e) (or the error message)
before using them. Specifically update the except blocks catching
(json.JSONDecodeError, ValueError) and OSError: call
util_error_sanitization(e_msg) to produce a safe_error and use safe_error in
logger.warning and in the ModelSessionStateResult(error=...), leaving other
fields (success, operation, correlation_id, error_code, files_affected)
unchanged.

In
`@src/omnibase_infra/nodes/node_session_state_effect/handlers/handler_session_index_write.py`:
- Around line 32-38: HandlerSessionIndexWrite is missing the required
handler_type and handler_category properties required by the handler contract;
add two read-only properties on the class named handler_type and
handler_category that return the appropriate EnumHandlerType and
EnumHandlerTypeCategory values (import them if necessary) so the class exposes
its type and category for registration/routing; ensure the properties are
implemented as simple getters on HandlerSessionIndexWrite and return the correct
enum members consistent with other handlers.
- Around line 109-116: The exception handler in the session index write path
currently embeds the raw OSError text into the ModelSessionStateResult.error,
potentially leaking sensitive paths; update the except OSError as e block in
handler_session_index_write to pass e through the sanitizer in
omnibase_infra.utils.util_error_sanitization (e.g. call the provided sanitize
function) and use the sanitized string when constructing
ModelSessionStateResult(error=...), leaving error_code and other fields
unchanged; ensure you import the sanitizer and apply it to the error message
before populating the error field (you may keep the existing logger.warning but
do not return the raw e).

In
`@src/omnibase_infra/nodes/node_session_state_effect/handlers/handler_stale_run_gc.py`:
- Around line 28-50: The HandlerStaleRunGC class is missing the required
handler_type and handler_category properties used by the handler
registration/routing system; add two read-only `@property` methods on
HandlerStaleRunGC: handler_type() -> EnumHandlerType and handler_category() ->
EnumHandlerTypeCategory that return the correct enum members for this
garbage-collect stale-run handler (choose the appropriate EnumHandlerType and
EnumHandlerTypeCategory values used elsewhere in the codebase), and import
EnumHandlerType and EnumHandlerTypeCategory if not already imported so the class
exposes these properties for the handler contract.

In `@tests/unit/nodes/node_session_lifecycle_reducer/test_state_transitions.py`:
- Around line 8-17: Add a module-level pytest marker so the file is classified
as a unit test: declare pytestmark = pytest.mark.unit at module scope (after the
existing imports) in tests referencing EnumSessionLifecycleState and
ModelSessionLifecycleState; this uses the existing pytest import and ensures all
tests in the module are marked as unit tests.
🧹 Nitpick comments (6)
tests/unit/nodes/node_session_state_effect/test_handlers.py (2)

16-16: Unused import: timezone.

The timezone import is not used anywhere in this file; only UTC is used.

🧹 Proposed fix
-from datetime import UTC, datetime, timezone
+from datetime import UTC, datetime

49-50: Consider adding @pytest.mark.unit marker to test classes.

Per coding guidelines, test files should use pytest markers for classification. Adding @pytest.mark.unit to test classes ensures proper test categorization and filtering.

🏷️ Proposed fix example
+@pytest.mark.unit
 class TestHandlerSessionIndexRead:
     """Tests for HandlerSessionIndexRead."""

Apply similarly to other test classes: TestHandlerSessionIndexWrite, TestHandlerRunContextRead, TestHandlerRunContextWrite, TestHandlerStaleRunGC, TestConcurrentPipelineIsolation.

As per coding guidelines: "Use pytest markers: @pytest.mark.unit, @pytest.mark.integration, @pytest.mark.slow, @pytest.mark.chaos, @pytest.mark.serial for test classification".

src/omnibase_infra/nodes/node_session_state_effect/models/model_session_index.py (1)

18-18: Unused import: timezone.

The timezone import is not used; only UTC is referenced throughout the file.

🧹 Proposed cleanup
-from datetime import UTC, datetime, timezone
+from datetime import UTC, datetime
src/omnibase_infra/nodes/node_session_state_effect/models/model_run_context.py (2)

12-12: Unused import: timezone.

The timezone import is not used; only UTC is referenced.

🧹 Proposed cleanup
-from datetime import UTC, datetime, timezone
+from datetime import UTC, datetime

49-52: Consider immutability implications for metadata field.

The model is frozen, but dict[str, object] is inherently mutable. While Pydantic prevents field reassignment, external code could still mutate the dict's contents if a reference is held. The with_metadata method correctly creates a shallow copy, but the copied dict could still contain mutable values.

For strict immutability, consider using MappingProxyType for read-only access or documenting that callers must not mutate the dict. Given that the transition helpers follow the pure pattern, the current approach is acceptable for pragmatic use.

As per coding guidelines: "use tuple for immutable collections in frozen models."

src/omnibase_infra/nodes/node_session_state_effect/contract.yaml (1)

32-39: Input model may not accurately represent all operations.

The input_model is declared as ModelSessionIndex, but this node handles multiple operations with different input requirements:

  • session.index.read requires no input
  • session.run.read requires run_id
  • session.gc requires no input (or optional TTL)

Consider whether the contract schema supports operation-specific input models, or add a clarifying comment that the actual input varies per operation and is documented in io_operations.

… code hygiene

- Fixed: path traversal vulnerability in run_id — added field_validator
  rejecting '..', '/', '\', '\0' in ModelRunContext.run_id, plus
  pre-validation in HandlerRunContextRead for raw string parameter
- Fixed: blocking flock inside async — all 5 handlers now offload sync
  I/O to asyncio.to_thread() to avoid blocking the event loop
- Fixed: POSIX-only fcntl import — guarded with sys.platform check,
  raises RuntimeError on Windows at construction time
- Fixed: unused timezone imports in 3 model/handler files
- Fixed: __all__ alphabetical order in enums/__init__.py
- Fixed: misleading YAML comment indentation in validation_exemptions
- Fixed: GC handler docstring incorrectly claiming session index cleanup

Review iteration: 1/10
- Fixed: unused timezone imports in test_handlers.py and test_models.py
- Fixed: missing test coverage for path traversal rejection — added
  parametrized tests covering ../, /, \, and null byte in both
  ModelRunContext validator and HandlerRunContextRead handler

Review iteration: 2/10
…lock perms

- Fixed: handler_session_index_write.py:122 - fd leak if flock(LOCK_UN) raises
- Fixed: handler_session_index_write.py:96 - lock file now uses explicit 0o600 mode
- Fixed: model_session_index.py:38 - recent_run_ids capped at MAX_RECENT_RUNS=1000
- Fixed: model_session_lifecycle_state.py - with_* methods now enforce FSM preconditions
- Fixed: infra_validators.py:455 - comment corrected dict[str, Any] -> dict[str, object]

Review iteration: 1/10
…symlink safety, docs

- Added path traversal defense-in-depth in HandlerRunContextWrite (parity with read handler)
- Added symlink/resolve guard in HandlerStaleRunGC to prevent deletion outside runs dir
- Documented intentional lock file persistence in HandlerSessionIndexWrite
- Documented metadata dict mutability limitation in ModelRunContext

Review iteration: 1/10
…id allowlist, resolve guards

- Added max_deletions cap (500) to HandlerStaleRunGC to bound per-pass work
- Replaced json.loads(model_dump_json()) with model_dump(mode="json") in both write handlers
- Switched run_id validator from denylist to allowlist (alphanumeric + dot/hyphen/underscore)
- Added resolve()-based path containment check in HandlerRunContextRead
- Aligned handler-level validation to use same allowlist regex
- Documented malformed GC deleted_ids as best-effort for callers

Review iteration: 2/10
… defensive metadata copy

- Fixed FD leak if os.fdopen() raises after tempfile.mkstemp() in both write handlers
- Removed exists() gate on resolve() containment check to eliminate TOCTOU gap
- Added defensive metadata dict copy in with_status() (parity with with_metadata())

Review iteration: 3/10
…dge-case flakiness

- Changed is_stale() from strict > to >= for correct boundary semantics
- Prevents potential flakiness when ttl_seconds=0.0 and clock resolution is coarse

Review iteration: 4/10
…on catch, metadata docs

- Added resolve()-path defense-in-depth check in handler_run_context_write
  (parity with read handler's two-layer path traversal guard)
- Added except-Exception clause in handler_run_context_write and
  handler_session_index_write to catch non-OSError exceptions and return
  structured ModelSessionStateResult instead of unhandled tracebacks
- Elevated metadata mutability warning to class-level docstring in
  ModelRunContext

Review iteration: 1/10
…d validation, GC symlink guard

- Narrow metadata from dict[str, object] to dict[str, StrictJsonPrimitive] (omnibase_core)
  for lossless JSON round-trip and frozen-model immutability
- Use logger.exception() in broad except blocks (run_context_write, session_index_write)
  to capture full tracebacks for unexpected errors
- Return None from session_index_read on parse/IO error to prevent silent overwrite
  with empty default index
- Add field_validator + with_run_added guard on ModelSessionIndex for run_id path safety
- Skip symlinks explicitly in stale run GC to prevent stem/data mismatch
- Document best-effort semantics of GC deleted_ids in docstring
- Reorder path traversal tests before write section; fix malformed JSON assertion
…C stem cross-check

- Fixed: model_session_index.py:38 - Added field_validator for active_run_id
  to apply the same _RUN_ID_PATTERN allowlist, preventing path-traversal via
  model_validate() from deserialized JSON
- Fixed: handler_stale_run_gc.py:121 - Added missing_ok=True to unlink() on
  stale run path for consistency with malformed file cleanup
- Fixed: handler_stale_run_gc.py:122 - Cross-check run_file.stem vs ctx.run_id,
  log warning and include both in deleted_ids if they differ

Review iteration: 1/10

@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: 3

🤖 Fix all issues with AI agents
In
`@src/omnibase_infra/nodes/node_session_lifecycle_reducer/models/model_session_lifecycle_state.py`:
- Around line 89-160: Replace plain ValueError raises in the lifecycle
transition methods (e.g., with_run_created, with_run_activated, with_run_ended,
with_reset) with exceptions created via
ModelInfraErrorContext.with_correlation(...). Use the incoming correlation_id if
available or uuid4() when missing, pass transport_type and operation (use a
clear transport_type like "internal" or the node name and operation equal to the
method name, e.g., "with_run_activated"), and ensure the message is sanitized
via omnibase_infra.utils.util_error_sanitization (sanitize_message) before
putting it into the error context; then raise/return the context-created
exception so the node-layer receives correlation-aware, sanitized errors.
- Around line 167-211: The can_activate_run and can_end_run predicates currently
only check status and should also guard that a valid run_id exists; update
ModelSessionLifecycleState.can_activate_run and can_end_run to return True only
when status == EnumSessionLifecycleState.RUN_CREATED (or RUN_ACTIVE
respectively) AND self.run_id is not None (or use a more strict validation if
run_id must be a UUID), so malformed states with run_id=None cannot transition
to active/ended.

In
`@src/omnibase_infra/nodes/node_session_state_effect/handlers/handler_run_context_read.py`:
- Around line 115-139: Import the sanitizer from
omnibase_infra.utils.util_error_sanitization (e.g., sanitize_error_message) and
use it in the exception handlers in handler_run_context_read.py: call sanitized
= sanitize_error_message(e) and replace uses of the raw exception text in
logger.warning and the ModelSessionStateResult.error fields (the blocks that
create ModelSessionStateResult on JSONDecodeError/ValueError and on OSError) to
include sanitized instead of f"{e}"; keep run_id and correlation_id as-is.
🧹 Nitpick comments (7)
tests/unit/nodes/node_session_state_effect/test_models.py (3)

23-106: Missing @pytest.mark.unit marker on test class.

Per coding guidelines, test files should use pytest markers (@pytest.mark.unit, @pytest.mark.integration, etc.) for test classification. Add @pytest.mark.unit to TestModelSessionIndex and other test classes in this file.

🔧 Suggested fix
+@pytest.mark.unit
 class TestModelSessionIndex:
     """Tests for ModelSessionIndex."""

113-170: Test class TestModelRunContext also needs @pytest.mark.unit marker.

Same issue as above - add the pytest marker for proper test classification.


178-203: Test class TestModelSessionStateResult needs @pytest.mark.unit marker.

Additionally, the uuid4 import on lines 183 and 194 could be moved to the module-level imports for consistency.

🔧 Suggested fix for imports
 from datetime import UTC, datetime
+from uuid import uuid4
 
 import pytest

Then remove the inline from uuid import uuid4 statements inside the test methods.

tests/unit/nodes/node_session_state_effect/test_handlers.py (2)

49-86: Missing @pytest.mark.unit marker on test classes.

All test classes in this file (TestHandlerSessionIndexRead, TestHandlerSessionIndexWrite, etc.) should have @pytest.mark.unit decorator per coding guidelines for test classification.

🔧 Suggested fix example
+@pytest.mark.unit
 class TestHandlerSessionIndexRead:
     """Tests for HandlerSessionIndexRead."""

133-148: Good concurrent safety test, but consider adding assertion for data integrity.

The test verifies that JSON is valid after concurrent writes, but doesn't assert that the final state is consistent (one complete write won). Consider adding an assertion that recent_run_ids has exactly one entry matching one of the written values.

🔧 Optional enhancement
         # File should be valid JSON (one of the writes won)
         data = json.loads((state_dir / "session.json").read_text())
         assert "recent_run_ids" in data
+        # Verify the final state has exactly one run from one writer
+        assert len(data["recent_run_ids"]) == 1
+        assert data["recent_run_ids"][0] in [f"run-{i}" for i in range(10)]
src/omnibase_infra/nodes/node_session_state_effect/handlers/handler_stale_run_gc.py (1)

120-128: Consider logging the file age at debug level for non-stale files.

Currently, only stale files get logged. For debugging GC behavior, it might be helpful to log at debug level when files are checked but not deleted.

src/omnibase_infra/nodes/node_session_state_effect/models/model_run_context.py (1)

55-61: Consider documenting the defensive copy behavior for metadata.

The with_status method creates a defensive copy of metadata ({**self.metadata}), which is good practice. However, external code could still mutate the dict after construction if they hold a reference to the original. Since the model is frozen, this is a minor concern, but you might consider using MappingProxyType or documenting this limitation.

Also applies to: 104-110

…id validation, TOCTOU fix

- Fixed: model_session_index.py:135 - with_run_added() now clears
  active_run_id when it gets trimmed by MAX_RECENT_RUNS cap
- Fixed: handler_session_index_write.py - Added handle_read_modify_write()
  for atomic read-transform-write under flock, preventing lost-update
  races when concurrent pipelines modify the session index
- Fixed: model_run_context.py - Extracted validate_run_id() and
  RUN_ID_PATTERN into shared functions, eliminating 4-way duplication
  across models and handlers
- Fixed: handler_session_index_read.py:64 - Replaced exists()+read_text()
  TOCTOU with try/except FileNotFoundError for consistent default-index
  behavior when file is deleted mid-read

Review iteration: 2/10
…k, read consistency docs

- Fixed: test_validator_defaults.py:111 - Updated hardcoded assertion to
  match INFRA_MAX_UNIONS=121 with threshold history entry
- Fixed: handler_stale_run_gc.py:121 - Reordered to append run_id to
  deleted_ids before unlink, ensuring tracking even if post-delete
  operations fail unexpectedly
- Fixed: handler_session_index_read.py - Added Note section documenting
  that standalone reads may return pre-transaction state during
  concurrent handle_read_modify_write() operations

Review iteration: 3/10
- Fixed: handler_stale_run_gc.py:147 - Malformed file handler now tracks
  stem in deleted_ids before unlink, consistent with stale file path
- Fixed: handler_session_index_write.py:129 - _read_modify_write_sync now
  uses try/except FileNotFoundError instead of exists() + read_text()
  TOCTOU pattern

Review iteration: 4/10
…GC robustness

Major fixes:
- Add validate_run_id() at FSM boundary in with_run_created (defense in depth)
- Remove dead failure_reason field from lifecycle state model
- RMW read phase catches JSONDecodeError/ValueError, self-heals from corruption
- Defensive dict copy in ModelRunContext._freeze_metadata for frozen invariant

Minor fixes:
- Catch pydantic.ValidationError in run_context_read and stale_run_gc handlers
- Sort GC glob by mtime so oldest files are collected first under max_deletions
- Pre-resolve runs_dir in run_context_read for consistency with GC handler
- Add RUN_ID_PATTERN and validate_run_id to model_run_context __all__
- Fix docstring: "Ordered list" -> "Ordered tuple" in model_session_index
- Parametrize all 12 invalid transition tests (was 4)
- Document single-ID idempotency limitation in is_duplicate_event
- Remove redundant is_terminal from contract.yaml run_ended state
- Add flock serialization comment on concurrent RMW test
…ve consistency

- Fixed: handler_stale_run_gc.py - max_deletions now counts files not IDs
- Fixed: handler_stale_run_gc.py - deleted_ids appended only after unlink succeeds
- Fixed: model_run_context.py - _freeze_metadata docstring clarifies shallow copy sufficiency
- Fixed: handler_run_context_write.py - resolve() at construction matches read handler
- Fixed: handler_session_index_read.py - explicit ValidationError in except tuple
- Fixed: model_session_index.py - dedup run IDs on deserialization

Review iteration: 1/10
…stead of overcounting

- Fixed: handler_stale_run_gc.py - use unlink() + FileNotFoundError catch
  instead of unlink(missing_ok=True) to avoid incrementing files_deleted
  when another process concurrently removed the file

Review iteration: 2/10
…w sensitivity

- Fixed: handler_run_context_read.py - comment now describes structural
  comparison benefit, not incorrect TOCTOU claim
- Fixed: model_run_context.py - is_stale() docstring documents clock-skew
  sensitivity between writer and GC processes

Review iteration: 3/10
…handler docs, defensive-copy test

- Fixed: handler_session_index_write.py:136 - Add ValidationError to except clause in RMW path (matching read handler)
- Fixed: handler_run_context_write.py:139 - Document intentional catch-all pattern in crash-resilient handlers
- Fixed: handler_session_index_write.py:204,290 - Document intentional catch-all pattern
- Fixed: handler_stale_run_gc.py:141 - Document deleted_ids vs files_affected divergence in docstring
- Fixed: test_models.py - Add test_metadata_defensive_copy verifying _freeze_metadata isolation

Review iteration: 1/10
…sistency, empty-dict test

- Fixed: model_run_context.py:120 - _freeze_metadata now copies unconditionally (empty-dict edge case)
- Fixed: handler_run_context_read.py:138 - OSError handler sets files_affected=1 for consistency
- Fixed: handler_run_context_write.py:138 - OSError handler sets files_affected=1 for consistency
- Added: test_metadata_defensive_copy_empty_dict verifying empty-dict isolation

Review iteration: 2/10

@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: 3

🤖 Fix all issues with AI agents
In
`@src/omnibase_infra/nodes/node_session_state_effect/handlers/handler_session_index_write.py`:
- Around line 66-68: Replace the raw RuntimeError in the
HandlerSessionIndexWrite constructor with a raise that uses
ModelInfraErrorContext.with_correlation(...): call
ModelInfraErrorContext.with_correlation(transport_type=<appropriate transport>,
operation=<appropriate operation>, correlation_id=<generate new id if none>) to
build the context and pass it into the exception so the RuntimeError includes
the required context; specifically update the branch where fcntl is None to
raise using ModelInfraErrorContext.with_correlation and include transport_type
and operation values and an autogenerated correlation id when not provided.

In `@tests/unit/nodes/node_session_state_effect/test_handlers.py`:
- Around line 20-34: This test module is missing the pytest unit marker; add a
module-level pytestmark to classify the tests as unit tests by importing pytest
(if not already) and defining pytestmark = pytest.mark.unit near the top of
tests/unit/nodes/node_session_state_effect/test_handlers.py so the
handlers/models tests (HandlerRunContextRead, HandlerRunContextWrite,
HandlerSessionIndexRead, HandlerSessionIndexWrite, HandlerStaleRunGC,
ModelRunContext, ModelSessionIndex) are marked as unit tests.

In `@tests/unit/nodes/node_session_state_effect/test_models.py`:
- Around line 7-16: The test module is missing a unit test marker; add a
module-level pytest mark by defining pytestmark = pytest.mark.unit at the top of
the file (import pytest if not present) so this test file is classified as unit
tests—update tests/unit/nodes/node_session_state_effect/test_models.py to
include the pytest import and set pytestmark for the module.
🧹 Nitpick comments (1)
src/omnibase_infra/validation/infra_validators.py (1)

455-456: Documentation gap: missing history entry for 121st union.

The comment on line 455 documents the 120th union (ModelRunContext.metadata), but INFRA_MAX_UNIONS is set to 121. The test file (test_validator_defaults.py) documents both additions:

  • 120: ModelRunContext.metadata
  • 121: Callable[[ModelSessionIndex], ModelSessionIndex] transform parameter

Consider adding a second history note for completeness:

📝 Suggested documentation fix
 # Note: OMN-2117 ModelRunContext.metadata uses StrictJsonPrimitive from omnibase_core (120th)
+# Note: OMN-2117 SessionIndexRead/Write Callable transform parameter (121st)
 INFRA_MAX_UNIONS = 121

Comment thread tests/unit/nodes/node_session_state_effect/test_handlers.py
Comment thread tests/unit/nodes/node_session_state_effect/test_models.py
…tion, test markers, docstrings

Address all PR #300 review feedback:

- Add handler_type/handler_category properties to all 5 session state handlers
- Sanitize error strings via sanitize_error_string() before returning
- Use ModelInfraErrorContext.with_correlation() for error context in
  handler_session_index_write and model_session_lifecycle_state
- Validate run_id against path traversal in handler_run_context_write
- Guard missing run_id before activation/end transitions
- Replace ValueError with RuntimeHostError for state transition errors
- Add debug logging for non-stale files in handler_stale_run_gc
- Add pytestmark and @pytest.mark.unit to all test files and classes
- Add data integrity assertion to concurrent safety test
- Add docstrings to key handler classes and methods
- Document metadata immutability trade-off and defensive copy pattern
- Note input model coverage gap in contract.yaml TODO
- Add 121st union history entry in infra_validators.py

@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: 2

🤖 Fix all issues with AI agents
In `@src/omnibase_infra/nodes/node_session_state_effect/contract.yaml`:
- Around line 31-40: The contract's input_model currently names
ModelSessionIndex but handlers accept varied input shapes (e.g., run_id: str,
ModelRunContext, RMW transform); update the contract to match actual handler
signatures by either (a) defining a union/envelope type (e.g.,
ModelSessionStateInput) that includes ModelSessionIndex, run_id,
ModelRunContext, and transform payloads and set input_model to that union, or
(b) add per-operation input_model entries mapping each operation to its correct
model and remove the TODO; reference the existing input_model declaration and
the handler types (ModelSessionIndex, ModelRunContext, run_id, RMW transform)
when making the change so validation and consumer docs align.

In
`@src/omnibase_infra/nodes/node_session_state_effect/handlers/handler_run_context_read.py`:
- Around line 137-191: The code currently treats a FileNotFoundError from
run_path.read_text() as a generic OSError and returns failure, but the contract
for run_context_read requires missing files to return success=True and
files_affected=0; modify the exception handling around the read/parse block in
handler_run_context_read so you explicitly catch FileNotFoundError (from
run_path.read_text) and return (None, ModelSessionStateResult(success=True,
operation="run_context_read", correlation_id=correlation_id, files_affected=0));
keep the existing JSON/ValidationError handling for parse errors and leave the
broader OSError handler for other I/O errors (ensure FileNotFoundError is
handled before the OSError except block).

Comment thread src/omnibase_infra/nodes/node_session_state_effect/contract.yaml Outdated
…tion, test markers, docstrings

- Sanitize error messages in handler_run_context_read/write (remove raw run_id from output)
- Handle TOCTOU FileNotFoundError race in handler_run_context_read
- Add null byte validation for run_id in handler_run_context_write
- Guard can_activate_run/can_end_run against missing run_id in lifecycle state model
- Reorder transition guards so run_id-missing error takes precedence
- Add per-handler input_model declarations to contract.yaml
- Correct 121st union history entry in infra_validators.py
- Clean up redundant inline uuid4 imports in test_models.py
@jonahgabriel
jonahgabriel merged commit 63ff3b3 into main Feb 11, 2026
15 checks passed
@jonahgabriel
jonahgabriel deleted the jonah/omn-2117-omnibase_infra-p1-canonical-state-nodes-session-state-effect branch February 11, 2026 18:25
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