Skip to content

Conversation

@pull
Copy link

@pull pull bot commented Jan 1, 2026

See Commits and Changes for more details.


Created by pull[bot] (v2.0.0-alpha.4)

Can you help keep this open source service alive? 💖 Please sponsor : )

…ergence (#2270)

## Summary

This PR implements a complete overhaul of the scheduler with pull-based execution and adds comprehensive debugging tools to the shell.

### Pull-Based Scheduler

- **Lazy execution model**: Computations only run when effects need their outputs, reducing unnecessary work
- **Topological ordering**: Dependencies run in correct order with parent-child action ordering
- **Cycle-aware convergence**: Uses settle loop with smart cycle detection - fast cycles (<16ms) converge synchronously, slow cycles yield between iterations
- **Dynamic dependency discovery**: Actions can specify custom `populateDependencies` for optimized scheduling (e.g., ifElse only depends on condition initially)
- **Performance controls**: Debounce/throttle APIs with auto-detection for slow actions (>50ms)

### New Debugger UI

#### Scheduler Tab (New!)
Visual debugging tool for understanding reactive dependency graphs:

- **Graph View**: Interactive DAG visualization showing effects, computations, and their dependencies
  - Color-coded nodes (red effects, blue computations, white inputs)
  - Parent-child relationship edges with legend
  - Reads/writes diagnostics on each node
  - Zoom, pan, and collapse controls
  - Dirty/pending state indicators

- **Table View**: Sortable list of all scheduler actions
  - Performance stats (run count, avg/max duration)
  - Baseline snapshots for comparing before/after
  - Debounce/throttle badges
  - Source location links
  - Inactive node tracking

#### Loggers Tab (New!)
Runtime logging control panel:

- **Granular log control**: Enable/disable individual loggers at runtime
- **Level selection**: Set log level per logger (error, warn, info, debug)
- **Hierarchical organization**: Loggers grouped by namespace
- **Live message counts**: Real-time counters for each logger
- **Mute/unmute controls**: Quick toggles with visual feedback

### Key Features

**Pull-Based Core:**
- Dirty tracking (`dirty` Set, `markDirty`, `isDirty`, `clearDirty`)
- Effect/computation distinction with `isEffect` option in `subscribe()`
- Reverse dependency graph for transitive dirty collection
- `Cell.pull()` method for demand-driven value retrieval

**Builtin Optimizations:**
- `RawBuiltinResult` interface for builtins to customize scheduling behavior
- `ifElse` only depends on condition, not both branches
- `navigateTo` self-declares as effect

**Performance Diagnostics:**
- Action stats tracking by source location
- `mightWrite` tracking for potential writes
- `writersByEntity` index for O(1) dependency lookup
- Telemetry events for dependency updates

## Test Plan

- [x] All scheduler tests pass (12 test groups, 88 steps)
- [x] All 144 generated-patterns integration tests pass
- [x] Backwards compatibility with `pullMode=false` verified
- [x] Cycle convergence behavior covered by automated tests
- [x] Debounce/throttle behavior covered by automated tests
- [x] ifElse branch selection test added

## Migration Notes

- `subscribe()` removes legacy boolean signature; use options object with `isEffect` and `rescheduling`
- Prefer `cell.pull()` over `idle()+get()` for demand-driven reads
- Add a sink to long-lived outputs you want to keep reactive (e.g., charms)
- `get()` now accepts `{ traverseCells?: boolean }` for deep dependency capture

---

* feat(scheduler): implement pull-based scheduler phases 1 & 2

Phase 1 - Effect Marking:
- Add effect/computation tracking (effects, computations Sets)
- Modify subscribe() to accept isEffect option
- Mark sink callbacks as effects in subscribeToReferencedDocs()
- Add diagnostic APIs (getStats, isEffect, isComputation, getDependents)
- Build reverse dependency graph (dependents WeakMap)

Phase 2 - Pull-Based Core:
- Add dirty tracking (dirty Set, markDirty, isDirty, clearDirty)
- Add pullMode feature flag with enable/disable methods
- Modify storage change handler to branch on pullMode
- Add scheduleAffectedEffects() for effect discovery
- Implement pullDependencies() and runComputation() for pull mechanism
- Add topologicalSortDirty() for ordering dirty computations
- Update execute loop with pull mode assertion

Both phases include comprehensive tests and maintain backwards
compatibility with existing push-based scheduling (pullMode=false).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(scheduler): simplify pull-based scheduling execution model

Refactor Phase 2 implementation based on code review feedback:

- Remove pullDependencies() call from run() - run() just runs the action
- Change execute() to build work queue differently in pull mode:
  - Collect pending effects + all their dirty computation dependencies
  - Topologically sort the combined set
  - Run all using existing run() method
- Add collectDirtyDependencies() helper for transitive dirty dep collection
- Remove unused methods: pullDependencies(), topologicalSortDirty(),
  runComputation() (~165 lines removed)

This avoids the server roundtrip issue from awaiting commit() inside
runComputation(). Instead, all dirty dependencies are collected upfront
and run through the standard execution path.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(scheduler): remove legacy boolean signature from subscribe()

Remove the deprecated boolean third parameter from scheduler.subscribe().
The method now only accepts the options object format:

  subscribe(action, log, { scheduleImmediately?: boolean, isEffect?: boolean })

Updated all call sites across the codebase:
- packages/runner/src/scheduler.ts
- packages/runner/src/runner.ts
- packages/runner/test/scheduler.test.ts
- packages/shell/src/lib/iframe-ctx.ts
- docs/specs/pull-based-scheduler/scheduler-graph-investigation.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* remove "phase 1" "phase 2" comments

* lint, fmt

* docs(scheduler): update implementation plan for Phase 2 refactoring

Update Phase 2 tasks to reflect actual implementation:
- Document collectDirtyDependencies() approach in execute()
- Document removal of legacy boolean signature from subscribe()
- Remove references to pullDependencies()/runComputation() methods

Update Phase 3 tasks to align with new architecture:
- Cycle detection now happens during collectDirtyDependencies()
- Cycle handling integrated into execute() flow
- Updated method names and descriptions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): implement Phase 3 cycle-aware convergence

Add compute time tracking and cycle detection to the pull-based scheduler:

- Add ActionStats interface and actionStats WeakMap to track execution times
- Add recordActionTime() to measure action performance
- Add getActionStats() public API for diagnostics
- Add detectCycles() using Tarjan's algorithm for SCC detection
- Add collectStack for cycle detection during dependency collection
- Add convergeFastCycle() for synchronous convergence of fast cycles (<16ms)
- Add runSlowCycleIteration() for yielding slow cycles between iterations
- Modify execute() to detect and handle cycles before normal execution
- Add comprehensive tests for cycle detection, stats tracking, and convergence

Constants:
- MAX_CYCLE_ITERATIONS = 20
- FAST_CYCLE_THRESHOLD_MS = 16

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): implement Phase 4 debounce and throttle

Add debounce infrastructure for slow actions:
- setDebounce/getDebounce/clearDebounce API
- scheduleWithDebounce for delayed scheduling
- Auto-debounce detection for actions >50ms avg after 3 runs
- Debounce option in subscribe()

Add throttle (staleness tolerance) for computations:
- setThrottle/getThrottle/clearThrottle API
- isThrottled check in execute() to skip recently-run actions
- Throttled actions stay dirty for future pulls
- Throttle option in subscribe()

Key difference:
- Debounce: wait until triggers stop, run after T ms quiet
- Throttle: accept staleness up to T ms, skip if ran recently

All 111 tests pass (19 new tests for debounce/throttle).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs(scheduler): add Phase 5 push-triggered filtering plan

New phase before migration that combines pull and push mode strengths:
- Pull mode builds conservative "might need to run" work set
- Push mode knows what actually changed (via determineTriggeredActions)
- Running their intersection gives precision without losing pull semantics

Key components:
- Track "mightWrite" per action (accumulated historical writes)
- Track "pushTriggered" set per execution cycle
- Filter work set to skip actions not triggered by actual changes
- Edge cases: scheduleImmediately, first run, cycles bypass filter

Renumbers old Phase 5 (Migration) to Phase 6.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): implement Phase 5 push-triggered filtering

Combine pull mode's conservative work set with push mode's precision:
- Pull mode builds superset of what might need to run
- Push mode knows what actually changed via storage notifications
- Run intersection: only actions triggered by actual changes

Implementation:
- mightWrite WeakMap: accumulates all paths an action has ever written
- pushTriggered Set: tracks actions triggered by storage changes each cycle
- scheduledImmediately Set: tracks actions that bypass filtering
- shouldFilterAction(): decides whether to skip an action
- filterStats: tracks filtered vs executed counts for diagnostics

Bypass conditions (action always runs):
- Scheduled with scheduleImmediately: true
- First run (no prior mightWrite)
- Triggered by actual storage change (in pushTriggered)

Diagnostic API:
- getMightWrite(action): inspect accumulated writes
- getFilterStats(): get { filtered, executed } counts
- resetFilterStats(): reset statistics

All 112 tests pass (7 new tests for filtering).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Update docs/specs/pull-based-scheduler/scheduler-implementation-plan.md

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* fix(scheduler): fix fast cycle convergence and add missing test assertions

- Fix convergeFastCycle to check both dirty AND pending sets for cycle
  members (actions get re-added to pending when they write, not dirty)
- Call handleError when fast cycle iteration limit is reached (was only
  logging before, inconsistent with slow cycle behavior)
- Clean up both dirty and pending after hitting limit to stop the cycle
- Fix "should enforce iteration limit" test to subscribe both actions
  before awaiting idle (required for cycle detection)
- Add assertion to verify error handler is called on cycle limit
- Fix "should detect multiple independent cycles" test to properly
  declare read/write dependencies (was using empty arrays)
- Change meaningless toBeGreaterThanOrEqual(0) to toBe(2) assertion
- Disable memory provider logger

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fmt

* feat(cell): add pull() method for demand-driven value retrieval

Add Cell.pull() method that registers a temporary effect and waits for
the scheduler to process it. This ensures all dependencies are computed
before returning the value, providing consistent behavior across both
push and pull scheduling modes.

Key features:
- Registers as effect with scheduleImmediately: true
- Waits for scheduler.idle() before resolving
- Unsubscribes after value is retrieved (one-shot)
- Works in both push mode (equivalent to idle() + get())
  and pull mode (triggers dependency computation)

Use case: Replace `await idle(); cell.get()` pattern with cleaner
`await cell.pull()` that works correctly in pull-based scheduling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* lint

* feat(scheduler): enable pull mode by default and fix tests

Enable pull-based scheduling by default (pullMode = true) and update
all tests that assume push-mode behavior to explicitly disable pull mode.

Tests updated:
- scheduler.test.ts: basic scheduler, event handling, reactive retries,
  cycle-aware convergence describe blocks
- runner.test.ts: runRecipe, setup/start describe blocks
- recipes.test.ts: Recipe Runner describe block
- when-unless.test.ts: when and unless describe block
- llm-dialog.test.ts: llmDialog describe block
- fetch-data-mutex.test.ts: fetch-data mutex describe block
- cell.test.ts: Cell describe block
- html-recipes.test.ts: recipes with HTML describe block

The pattern for push-mode tests is to add:
  runtime.scheduler.disablePullMode();
in the beforeEach hook.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix pull scheduler dirty propagation and dependents

* feat(scheduler): disable pull mode by default and migrate tests to use .pull()

- Change pullMode default to false in scheduler for stability
- Add deepTraverse helper to Cell.pull() for schemaless cells
- Remove disablePullMode() from test setup in cell, llm-dialog,
  fetch-data-mutex, and html-recipes tests
- Migrate tests to use .pull() instead of runtime.idle() + .get()
- Keep explicit push mode tests in scheduler.test.ts and cell.test.ts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* lint

* remove debugging output

* change fetch test to pull result to trigger action

* fix(scheduler): improve pull mode cycle detection and error recovery

- Use mightWrite in collectDirtyDependencies to handle error recovery:
  After a computation throws before writing, mightWrite preserves
  knowledge of what paths it could write, ensuring it's still pulled
  when effects need its output

- Add path overlap check in getSuccessorsInWorkSet to fix spurious
  cycle detection: Previously only compared space/id, now also checks
  arraysOverlap(write.path, read.path) to avoid false positives

- Update cell.test.ts to use pull() instead of runtime.idle() + get()
  for the argumentCell test case

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(runner): pull handler-returned recipes and add persistent effect test

- Add resultCell.pull() when handlers return recipes to ensure execution
  (nothing else would read from them by default)
- Add test verifying pull() doesn't create persistent effects after completion

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(runner): track potential writes in ReactivityLog for diffAndUpdate

Add markReadAsPotentialWrite metadata marker to track reads that may
result in writes. This is useful for operations like diffAndUpdate
which reads values to compare before conditionally writing them back.

Changes:
- Add markReadAsPotentialWrite metadata marker (scheduler.ts)
- Extend ReactivityLog type with optional potentialWrites array
- Update txToReactivityLog to populate potentialWrites when flag is set
- Use markReadAsPotentialWrite in diffAndUpdate so Cell.set tracks
  all properties it compares, even ones that don't actually change

This enables the scheduler to know which paths an action might write to,
not just which paths it actually wrote to.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* update mightWrite with potential writes as well

* docs(scheduler): add Phase 5b for parent-child action ordering

Add implementation plan for tracking parent-child relationships between
actions to ensure proper execution order in pull mode:

- Parent actions must run before their children
- When parent unsubscribes children, remove them from work set
- Prevents stale execution when lifts return recipes that get replaced

This addresses the two failing recipe tests in pull mode:
- "should execute recipes returned by handlers"
- "should handle recipes returned by lifted functions"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): implement Phase 5b parent-child action ordering

Track parent-child relationships between actions to ensure proper execution
ordering in pull mode. When a child action is created during parent execution,
the parent must run first so it can potentially unsubscribe stale children.

Implementation:
- Add executingAction, actionParent, actionChildren tracking properties
- Record parent-child relationship in subscribe() when called during action execution
- Clean up relationships in unsubscribe()
- Pass actionParent to topologicalSort() to add parent→child edges
- Track executingAction in run() method

Tests added for:
- Parent executes before child
- Child skipped when parent unsubscribes it
- Parent-child ordering when both become dirty
- Nested grandparent-parent-child ordering
- Cleanup on unsubscribe

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): enable pull mode by default and fix handler-returned recipes

- Enable pull mode by default in scheduler (pullMode = true)
- Fix handler-returned recipes to use .sink() instead of .pull() so they
  become effects that re-run when inputs change
- Update test to verify handler-returned recipes continue running as
  long-lived charmlets

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(tests): add sinks to keep charms reactive in pull mode

In pull mode, charms need an effect (sink) pulling from them to stay
reactive when inputs change. Tests that set values externally and expect
UI updates need to create a sink on the result cell.

This is the correct approach rather than adding a blanket sink to all
recipes - the test explicitly declares its intent to observe reactivity.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(tests): use pull() in pattern harness for pull mode compatibility

In pull mode, .get() returns cached values without triggering
computation of dirty dependencies. Changed the pattern harness to use
.pull() which properly establishes an effect and waits for the
scheduler to compute all transitive dependencies.

This fixes 27/28 failing generated-patterns integration tests.
One test (counter-when-unless-operators) still fails due to a
deeper issue with nested lift computations through references.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(scheduler): use mightWrite for dependency chain and topological sort

When a lift function returns undefined on first run, no write is recorded
in the journal. This broke the dependency chain in pull mode because:
1. updateDependents only looked at actual writes
2. topologicalSort only used actual writes for ordering

Fix: Initialize mightWrite from declared writes during subscribe, and use
mightWrite (instead of just log.writes) in both updateDependents and
topologicalSort.

Adds test case that reproduces the nested lift scenario where inner lift
initially returns undefined.

Fixes counter-when-unless-operators test which has nested lifts where
the inner lift initially returns undefined.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fmt

* feat(builtins): add isEffect option to raw() for effect built-ins

- Add `isEffect` field to Module interface
- Add optional `options` parameter to `raw()` with `isEffect` flag
- Pass `isEffect` through to scheduler.subscribe() in instantiateRawNode
- Mark navigateTo as an effect since it triggers navigation side effects

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(llm-dialog): mark as effect and pull dependencies in startRequest

llmDialog is an effect (triggered by user action), not a computation.
In pull mode, it needs to ensure its dependencies are computed before
reading them.

Changes:
- Mark llmDialog as isEffect: true in registration
- Add pulls for inputs, pinnedCells, and their referenced cells
  at the start of startRequest

The helper functions (buildToolCatalog, buildAvailableCellsDocumentation)
remain synchronous so they can be shared with llm/generateText/generateObject
which are computations that need reactive dependency tracking.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(llm-dialog): mark as effect for pull mode support

llmDialog is an effect (triggered by user action), not a computation.
Mark it as isEffect: true in registration.

Note: Explicit pull() calls were removed because they conflict with
the handler's transaction lifecycle. The handler runs synchronously
with a transaction, but pull() is async. A better solution for
pulling dependencies in effect handlers is needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(llm-dialog): decouple startRequest from handler transaction

- Remove tx parameter from startRequest, use pull() and editWithRetry()
  instead to support pull-based scheduling
- Pull inputs, pinnedCells, and individual context/pinned cells at start
  of startRequest to ensure they're computed in pull mode
- Use editWithRetry() for the write to result.pinnedCells
- Remove isEffect marking since llmDialog is event-handler driven, not
  an effect

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs(specs): add userland handler pull design for pull-based scheduler

Design spec for ensuring event handler inputs are current in pull mode.

Key decisions:
- Treat handlers as one-time actions in the unified scheduler loop
- Topological sort naturally orders handler inputs before handlers
- Per-stream event queues preserve FIFO ordering
- Dependency re-validation handles dynamic references
- Event handlers get priority among actions at same topo level
- Throttled/debounced deps allowed to be stale

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): pull handler dependencies before running in pull mode

Implements the userland handler pull design to ensure event handlers
don't run until their dependencies are computed.

Changes:
- Add traverseCells flag to validateAndTransform() and Cell.get() to
  recursively read into nested Cells and capture all dependencies
- Add populateDependencies callback to addEventHandler() for schema-based
  dependency discovery without coupling scheduler to schema module
- In pull mode, check if handler dependencies are dirty before running;
  if so, re-queue the event and process pending actions first
- Use global FIFO ordering for events across all streams

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fmt

* test(scheduler): add tests for handler dependency pulling

Add tests verifying that in pull mode, event handlers wait for their
computed dependencies to be pulled before running:

- Handler as only reader of computed output
- Multiple dirty dependencies pulled before handler runs
- Chained dependencies (source -> computed1 -> computed2 -> handler)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(scheduler): add test for handler A triggering lift read by handler B

Tests the scenario where:
- Handler A writes to a lift's input and queues an event to handler B
- Handler B depends on the lift's output (via populateDependencies)
- The scheduler should pull the lift before running handler B

This validates that handler B sees the fresh lift output (10) rather
than the stale value (0).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(scheduler): check mightWrite to find actions that write to dependencies

When checking handler dependencies in pull mode, also iterate over dirty
actions and check their mightWrite to find actions that WRITE to the
entities the handler reads. Previously only checked triggers which maps
actions by what they READ.

Also updates the dynamic lift test to:
- Use scheduleImmediately: false for the lift (tests cold-start pull)
- Send a link to liftOutput as the event to handler B
- Handler B has populateDependencies that reads liftOutput

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): pass event to populateDependencies callback

Update populateDependencies to receive the event value, allowing handlers
to resolve dependencies from links in the event itself.

Changes:
- EventHandler.populateDependencies now takes (tx, event) instead of (tx)
- Store event value in eventQueue alongside the action closure
- Test updated to use runtime.getImmutableCell() to resolve the event
  link and register the dependency dynamically
- Removed incorrect triggers check (was looking for actions that READ
  from the same entity, but we need actions that WRITE to it)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(runner): include event in populateDependencies for handler dependency tracking

- recipe-binding.ts: Use isWriteRedirectLink instead of isLegacyAlias to handle
  both legacy $alias format and new sigil link format when writing lift outputs
- cell.ts: Use getAsWriteRedirectLink in export() for proper cell serialization
- runner.ts: Extract mergeEventIntoInputs helper and use it in populateDependencies
  so the scheduler can discover dependencies from event data (e.g., cell references
  passed as events)
- Add test for handler A creating lift and sending output to handler B

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(scheduler): rename scheduleImmediately to rescheduling with inverted semantics

The scheduler's subscribe() option has been renamed and its semantics inverted:

Old: scheduleImmediately?: boolean (default: false)
  - true  = "run now"
  - false = "just register triggers, wait for changes"

New: rescheduling?: boolean (default: false)
  - false = "first time → schedule immediately" (default)
  - true  = "action already ran → just register triggers"

This makes the common case (first-time subscription) require no option,
and explicitly marks the re-subscription case after an action completes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): implement pull-based scheduling for lazy computations

Pull-based scheduling makes computations lazy - they only run when an
effect (sink, pull, event handler) needs their value. This prevents
unnecessary computation and aligns with reactive programming principles.

Key changes:
- Effects now trigger queueExecution() when subscribed
- idle() resolves based on scheduled flag, not pending size
- subscribe() with rescheduling:false adds to dirty+pending without
  immediate scheduling (computations wait for pull)
- Removed scheduledFirstTime bypass in shouldFilterAction
- Tests updated to call pull() to trigger computation chains

Known issue: workSet filtering has || true workaround - removing it
breaks "should not run lifts until something pulls" test. The
collectDirtyDependencies mechanism needs investigation to properly
find computation dependencies from effect reads.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(scheduler): properly handle pull-based scheduling with event dependencies

Key fixes for pull-based scheduling:

1. workSet now only includes effects (not all pending computations)
   - Computations are lazy and only run when pulled by effects
   - This makes "should not run lifts until something pulls" test pass

2. Event handlers that have dirty dependencies now work correctly
   - When an event is deferred due to dirty deps, those deps are tracked
   - eventBlockingDeps are added to workSet so they run before the event
   - This makes "should wait for lift before handler" test pass

3. idle() resolution based on effectivePendingSize
   - In pull mode, idle resolves when no effects are pending
   - Orphaned computations don't block idle resolution

Known issue: 3 tests fail due to broken dependency chain in nested recipes
(outer lift doesn't read inner lift's cell). This is a dependency tracking
issue at the recipe/cell layer, not a scheduler issue.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(scheduler): complete pull-based scheduling with dynamic dependency discovery

Replace static ReactivityLog with PopulateDependencies callbacks that discover
dependencies dynamically. This ensures computations only run when pulled by
effects that need their outputs.

Key changes:
- subscribe() now takes a PopulateDependencies callback instead of ReactivityLog
- Add resubscribe() for re-subscribing after an action completes
- Discover dependencies during execute() before running actions
- Track potentialWrites in reactivity logs for bridging
- Fix findAllWriteRedirectCells() to follow sourceCell chains

This completes the lazy evaluation model where computations are only executed
when their outputs are actually needed by effects, reducing unnecessary work.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(scheduler): remove redundant pushTriggered filtering

The pushTriggered tracking and shouldFilterAction logic was redundant
with the existing pending/dirty set checks. Verified by testing that
commenting out shouldFilterAction had no effect on behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(scheduler): track throttled actions in filterStats

Count throttled actions as filtered in filterStats for diagnostics.
This provides visibility into how often throttling kicks in.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(scheduler): remove redundant .writes fallbacks

mightWrite always contains everything in dependencies.writes (and more),
so the fallbacks to .writes were never triggered.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs(scheduler): clarify mightWrite vs dependencies.writes distinction

mightWrite is cumulative (all paths ever written + potentialWrites) for
conservative dependency graph building. dependencies.writes is current
run only, used for ordering within an execution cycle where historical
writes would create false dependency edges.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(module): attach source location to function implementations

Parse V8 stack traces to extract the caller's file:line:col and attach
it to function implementations via .src property. Works in both Deno
and Chrome for debugging purposes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(scheduler): replace Tarjan's cycle detection with settle loop

Remove explicit cycle detection (~200+ lines) in favor of simpler implicit handling:

- Remove detectCycles(), convergeFastCycle(), runSlowCycleIteration()
- Remove slowCycleState WeakMap and related infrastructure
- Add parent-child aware topological sort (prefer parents in cycles)
- Add settle loop for conditional dependencies (ifElse pattern)
- Maintain true pull-based semantics (lazy evaluation preserved)

The settle loop re-collects dependencies after running computations,
handling cases where conditional logic changes which branch is active.
This is more robust than static cycle detection for dynamic dependency graphs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(scheduler): simplify action validity check in execution loops

Replace complex parent-child sibling tracking logic with a simple check
for whether an action is still scheduled (in computations or effects).
This handles all cases where running one action unsubscribes another
that's in the current workSet, without needing to track specific
parent-child relationships.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(scheduler): unify execution and settle loops

Consolidate the main execution loop and settle loop into a single
unified loop structure. This eliminates code duplication and ensures
all validation checks (throttle, loop counter, isStillScheduled) are
consistently applied in both the initial execution and settle phases.

The settle loop now wraps the entire execution logic:
- First iteration: processes initial seeds + their dirty deps
- Subsequent iterations: re-collects dirty deps from all effects

This fixes a bug where throttle checks were missing from the settle
loop.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(scheduler): fold pendingDependencyCollection into settle loop

Move the processing of newly subscribed actions (pendingDependencyCollection)
into the settle loop itself, eliminating the separate POST-LOOP section.

Now each settle iteration:
1. Processes pendingDependencyCollection (sets up deps for new subscriptions)
2. Collects dirty dependencies
3. Runs work

This removes ~100 lines of duplicated execution logic while maintaining
the same behavior for dynamic sub-recipes created during execution.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(scheduler): properly handle cycles and dirty effects

- Include dirty effects in initialSeeds so cycle-skipped or throttled
  effects get picked up on subsequent executions
- Track computations in early settle iterations for cycle detection
- When hitting max iterations, break cycles by:
  - Clearing dirty/pending for computations in early iterations that
    are still in the last workSet (likely part of the cycle)
  - Running all remaining dirty effects (so they're not lost)
  - Skipping throttled actions to preserve their dirty state
- Improve done-check to consider both pending AND dirty effects
- Remove TODO about debouncing - this cycle-breaking approach handles it

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(scheduler): add cycle-aware debounce for slow cycling actions

Adds adaptive debounce that triggers when an action runs 3+ times within
a single execute() call that takes >100ms total. The debounce delay is
set to 2× the execute time, helping break slow cycles earlier than the
hard 20-iteration limit.

- Track runs per action within each execute() cycle
- Apply adaptive debounce only to slow, repeatedly-running actions
- Respect noDebounce option for actions that opt out
- Add tests for the mechanism

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* test(scheduler): add comprehensive tests for cycle-aware debounce

Additional test coverage for the cycle-aware debounce feature:
- Verify existing higher debounce is not reduced by cycle debounce
- Verify multiple actions are tracked independently
- Verify run tracking resets between execute() cycles
- Verify clearDebounce removes cycle-applied debounce

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* style(scheduler): auto-format line wrapping

* chore(test): remove unused errorCaught variable

* Revert "chore(test): remove unused errorCaught variable"

This reverts commit 2cb3c2de0f5b9dc55372600bf12dfbc346f79218.

* chore(test): remove unused errorCaught variable

* docs(scheduler): consolidate spec docs into single README

Replace the original investigation and planning documents with a single
comprehensive README that describes the current implementation:

- Background on why pull-based scheduling
- Effects vs computations, dirty propagation
- Execution flow with settle loop
- Cycle handling (simplified approach, no Tarjan's)
- Event handler dependencies
- Debounce/throttle infrastructure
- Debugging guide

The removed documents are preserved in git history for reference.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(runner): use .name instead of .src for function source locations

Use Object.defineProperty to set function .name property instead of
custom .src property for tracking source locations. This makes the
source info visible in standard debugging tools and stack traces.

Actions and handlers now have prefixed names (action:path, handler:path)
for easier identification in scheduler logs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(patterns): ensure dynamically instantiated recipes have stable identity

When a lift dynamically instantiates recipes and pushes them to an array,
downstream lifts that read computed values from those recipes need the
recipes to have stable identity so the scheduler can properly track and
execute them before the downstream reads.

Changes:
- Use .for(index) on dynamically instantiated subtotalGroup recipes to
  give them stable identity based on array position
- Fix typing in totalItems lift to use { itemCount: number }[] instead
  of SubtotalGroupArgs[]
- Use Cell.push() instead of spread+set pattern in appendValueToList
- Remove unused imports and debug console.log statements

Also adds runner change to traverse cells in argument schema during
dependency capture, and a minimal repro test in recipes.test.ts that
demonstrates the failure mode with push-based scheduling (sink + idle).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* ct step pulls from result cell (fix CLI integration test)

* feat(shell): add Scheduler tab to debugger with dependency graph visualization

- Add SchedulerGraphView component using Dagre for graph layout
- Add getGraphSnapshot() to scheduler for exporting dependency graph
- Add scheduler graph types to telemetry (SchedulerGraphNode, SchedulerGraphEdge)
- Add historical edge tracking in DebuggerController
- Name all actions for debugging: sink, raw, action, pull, readResult
- Add .src backup property to actions since .name can be finicky
- Support push/pull mode toggle in graph view
- Show node stats (run count, avg time) with glow animation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat(shell): add zoom, collapse, and grouping to scheduler debug UI

- Add zoom controls (25%-200%) with reset button
- Add triggered node size boost when zoomed out below readable threshold
- Export parent-child relationships in scheduler graph snapshot
- Add collapse by parent functionality with +/- toggle
- Add visual grouping with background rectangles around parent-child clusters

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(scheduler): track parent-child relationships for sink actions

- Add parent-child tracking to resubscribe() method, fixing sinks
  created during parent action execution not having parentId set
- Add test verifying sink parent-child relationship tracking
- Fix zoom-around-center for scheduler graph view (preserve scroll
  position when zooming instead of resetting to top-left)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(shell): improve scheduler graph ID display

- Show last 4 chars of entity ID + path instead of "did:key:..." prefix
- Add SVG title element for full ID tooltip on hover
- Smarter truncation that preserves meaningful parts of action names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(shell): auto-collapse new parents in scheduler graph

- Always check for new parents with children to collapse, not just on first load
- Remove hasAutoCollapsed flag since we now check continuously

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(shell): infer parent-child relationships from entity IDs

When sinks don't have explicit parent relationships (created outside
of action execution, e.g., by the renderer), infer grouping by
matching entity IDs in action names. Sinks are grouped with the
computation/action that operates on the same entity.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): add reads/writes diagnostics to graph visualization

Show cell paths each action reads and writes in node tooltips.
This helps debug missing dependency edges by revealing:
- What cells an action declares as reads
- What cells an action declares as writes

When hovering over a node, the tooltip now shows:
- Full action ID
- Reads: list of cell paths
- Writes: list of cell paths (from mightWrite)

Also fixes unrelated type error in DebuggerView.ts.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): add input nodes for source cells in graph

Show cells that are read but not written by any action as input nodes.
These represent recipe inputs or external data sources.

- Add input node type (green, badge I)
- Find source entities: reads with no matching write
- Draw edges from input nodes to actions that read them
- Custom tooltip for input nodes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): show parent-child edges and add legend

* feat(scheduler): improve graph zoom and show all subscribed nodes

Zoom improvements:
- Continuous zoom with Ctrl/Cmd + mousewheel (Google Maps style)
- Zoom around cursor position
- Initial zoom to fit full graph on first load
- No min/max limits on zoom level

Show all nodes:
- Add scheduler.subscribe telemetry event
- Auto-refresh graph on subscribe/run/invocation events
- Shows nodes before they execute (pending state)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): add dependencies.update telemetry event

Emit scheduler.dependencies.update when updateDependents is called,
including the actual read/write cell paths. Graph auto-refreshes on
this event to capture dependency changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(shell): remove unused rect variable in zoom handler

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* workaround deno fmt bug

* fmt

* fix merge

* behave better when items is undefined

* feat(shell): add table view to scheduler debugger and fix issues

- Add table view with sortable columns (Type, Action, Runs, Total, Avg, Last)
- Fix browser stack trace parsing for http:// URLs and hashed filenames
- Add re-entrancy guard to prevent infinite telemetry update loop
- Add comprehensive parseStackFrame tests for browser formats

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): persist action stats by source location and show inactive nodes

- Change actionStats from WeakMap<Action> to Map<string> keyed by action ID
  (source location) so stats persist across action recreation
- Add getActionId() helper method to Scheduler for consistent ID generation
- Add inactive node type for actions with stats that are no longer registered
- Show inactive nodes in graph (gray) and table views to preserve historical data
- Add hierarchical parent-child grouping in table with aggregated stats
- Add expand/collapse for parent rows showing childrens individual times
- Add detail pane to table view (click row to see node details)
- Fix preview not showing in detail pane (access via module.implementation.preview)

This ensures slow actions remain visible in the debugger even after being
unsubscribed, making it easier to identify performance issues.

* feat(shell): add Loggers tab to debugger and improve log granularity

- Add new Loggers tab to DebuggerView with:
  - Reset baseline / Sample buttons for capturing log counts
  - Delta display showing changes relative to baseline
  - Enable/disable toggles for individual loggers
  - Expandable breakdown of log events by key

- Improve scheduler logging by replacing generic "schedule" keys:
  - schedule-resubscribe, schedule-resubscribe-path
  - schedule-unsubscribe
  - schedule-notification
  - schedule-change, schedule-change-skip, schedule-change-match
  - schedule-change-trigger, schedule-trigger
  - schedule-dep-collect, schedule-dep-error (and variants)
  - schedule-execute, schedule-execute-pull

- Improve storage logging:
  - storage-source-read, storage-source-parse
  - storage-datauri-parse
  - storage-commit-writes (specific event for commits with writes)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(shell): clarify logger toggle with mute/unmute icons

Change the logger enable/disable toggle to use mute/unmute speaker icons
(🔊/🔇) instead of filled/empty circles (●/○). This makes it clearer
that the toggle controls console output, not whether the logger is
actively tracking events.

The previous UI was confusing because:
- Loggers with high event counts showed as "disabled" (gray circle)
- Loggers with 0 events showed as "enabled" (green circle)

The new icons make it clear that you're muting/unmuting console output,
while event counting continues regardless.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(shell): add level selector and improve logger controls

Each logger row now has two controls:
- Level dropdown (debug/info/warn/error) to set minimum console output level
- Enable/disable toggle (●/○) to completely enable/disable console output

This matches the Logger class API which has both:
- `disabled` property (boolean) - master switch for console output
- `level` property (LogLevel) - minimum level filter for output

Changed back to ●/○ icons for enable/disable since it's now clear from
context that this controls the logger's enabled state, with the level
dropdown handling the filtering.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fmt

* add slow get logging

* fix logger

* feat(shell): improve scheduler view with table default and node navigation

- Make table the default view instead of graph
- When switching to graph with selection, zoom to 50% and center on node
- Add adjacent nodes (dependents/dependencies) to detail pane with click navigation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* deno lock

* perf(scheduler): add writersByEntity index for O(1) dependency lookup

- Add writersByEntity Map to index actions by the entities they write to
- Add actionWriteEntities WeakMap for cleanup tracking
- Rewrite updateDependents to use entity-based lookup instead of
  iterating all actions (O(reads × writers_per_entity) vs O(reads × actions))
- Add scheduler benchmarks to measure performance

Benchmark results show scheduler ops are fast (~6-7µs per subscribe/resubscribe).
The bottleneck is in cell creation (~730µs per cell), not scheduling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* bench(scheduler): add granular overhead benchmarks

Break down cell overhead into getCell vs set to identify bottlenecks:
- getCell only: ~19µs per call (fast)
- set on existing cells: ~590µs per call (slow)

The bottleneck is in cell.set(), not cell creation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* bench(scheduler): add cell internals and commit benchmarks

New benchmarks to isolate performance bottlenecks:

Cell layer internals (per 100 ops):
- createRef (simple): 1.0ms (10µs each)
- createRef (complex): 6.0ms (60µs each)
- resolveLink: 2.0ms (20µs each)
- tx.readValueOrThrow: 12.2ms (122µs each) <- expensive!
- diffAndUpdate: 21.7ms (217µs each) <- biggest cost

Storage commit:
- Empty commit: 507µs
- 100 raw tx.write + commit: 14.4ms (144µs/entity)
- Commit after 100 cell.set(): 44.8ms (448µs/entity)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* bench(runner): factor out storage benchmarks and add overhead microbenchmarks

- Create storage.bench.ts with storage layer benchmarks:
  - Write operations (tx.write, writeOrThrow, writeValueOrThrow)
  - Read operations (tx.read, readOrThrow, readValueOrThrow)
  - Entity creation overhead
  - Path depth comparison
  - Commit overhead
  - Overhead microbenchmarks

- Add overhead microbenchmarks to cell.bench.ts:
  - isRecord check
  - JSON.stringify comparison

- Remove cell/storage benchmarks from scheduler.bench.ts to keep
  it focused on scheduler-specific performance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(shell): add baseline/snapshot pattern to scheduler debugger

- Hide zoom controls when in table mode
- Replace Refresh/Clear History with Reset Baseline/Snapshot buttons
- Show delta stats (runs and time since baseline) in table and detail views
- Add totals for runs and time since baseline in toolbar
- Add [All|Δ] toggle to sort by lifetime totals vs delta since baseline

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* bench(runner): add entity creation breakdown benchmarks

Add microbenchmarks to isolate entity creation overhead:
- Write phase vs commit phase timing
- First write vs subsequent writes comparison
- Entity creation overhead measurement

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(scheduler): trigger execution on mode toggle

Queue scheduler execution when switching between pull and push modes
to ensure pending work is processed immediately after the mode change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(runner): fix URI type error in storage benchmarks

Inline template literal to preserve the `${string}:${string}` type
instead of assigning to a variable which widens to `string`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(scheduler): add array reactivity tests for pull mode

Add tests to verify array reactivity in pull mode:
- Direct sink on array with push/set works correctly
- Computation triggered by array push with explicit pull works
- Bug repro: sink on computed result not triggered when source array grows

The failing test demonstrates the root cause of the Notes UI bug where
the list doesn't update after creating a new note.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(scheduler): register triggers in subscribe() for immediate reactivity

- Add trigger registration in subscribe() when immediateLog is provided
- This ensures computations are notified of storage changes immediately
- Only set cancel function when triggers were actually registered
- Fix test to use fresh transaction for push (avoid consistency error)

The pull mode scheduler correctly marks computations dirty when source
cells change and uses scheduleAffectedEffects() to find and schedule
dependent effects.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(scheduler): mark effects dirty when they read from dirty computations

When an effect resubscribes, check if any non-throttled dirty computations
write to entities it reads. If so, mark the effect dirty so it can pull
those computations and see fresh data.

This fixes a bug where pattern remounts wouldn't see updated data:
- Pattern unmounts (computation unsubscribed)
- Data changes while unmounted
- Pattern remounts with NEW computation (different function reference)
- Sink runs once with stale cached data
- Sink calls resubscribe but never re-runs via scheduler

The fix uses the existing writersByEntity index for efficient lookup.
Throttled computations are skipped - they'll trigger via storage changes
when the throttle expires.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(shell): persist scheduler baseline stats across tab switches

- Move baselineStats from SchedulerGraphView component state to
  DebuggerController so it survives when switching debugger tabs
- Always show lifetime totals in toolbar (Total: X runs Yms)
- Show delta since baseline separately when baseline exists (Δ: +X runs)

Fixes issues where:
1. Reset baseline + snapshot data disappeared when switching to logger
   tab and back (component was destroyed/recreated)
2. Totals weren't showing all-time stats, only delta since baseline

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fmt

* feat(shell): show debounce/throttle badges in scheduler table view

- Add debounceMs and throttleMs fields to SchedulerGraphNode interface
- Populate these fields in getGraphSnapshot() from actionDebounce/actionThrottle maps
- Display colored badges in the table view action name column:
  - Purple "D:Xms" badge for debounced actions
  - Cyan "T:Xms" badge for throttled actions
- Badges include tooltips explaining the timing behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(runner): allow builtins to customize scheduling dependencies

Add RawBuiltinResult interface so builtins can return custom
populateDependencies and isEffect alongside their action. This enables
builtins like ifElse to only depend on the condition for initial
scheduling, avoiding unnecessary computation of unselected branches.

- Add RawBuiltinResult type with action, isEffect, populateDependencies
- Update instantiateRawNode to handle new return format
- Move navigateTo's isEffect inside its definition
- ifElse now only reads condition in populateDependencies
- Add test for ifElse branch selection behavior

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(shell): refactor template to avoid deno fmt crash

Extract renderBaselineStats helper to avoid deeply nested ternaries
in class attributes combined with multiline function calls, which
triggers a dprint-core indent tracking bug in deno fmt.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* turn off pull mode for merge

* fix(test): update potentialWrites tests to use nested paths

The tests incorrectly expected per-property potentialWrites entries,
but diffAndUpdate reads at the object level being written to. Updated
tests to use key("nested").set() to verify potentialWrites correctly
tracks the path being written to.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(scheduler): queue execution for computations in push mode

- In push mode, computations need to trigger execution when subscribed,
  not just effects. Without this, newly subscribed computations never run.

- In pull mode, when an effect resubscribes and there are pending
  computations whose dependencies haven't been collected yet, conservatively
  assume they might affect the effect and queue execution.

- Remove redundant queueExecution() call from sink() in cell.ts - the
  scheduler now handles this correctly in both push and pull modes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor scheduler logging and helpers

* fix scheduler trigger cleanup

* make scheduler telemetry serializable

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
@pull pull bot locked and limited conversation to collaborators Jan 1, 2026
@pull pull bot added the ⤵️ pull label Jan 1, 2026
@pull pull bot merged commit 5c2e391 into ExaDev:main Jan 1, 2026
1 check failed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant