Skip to content

Conversation

@pull
Copy link

@pull pull bot commented Dec 10, 2025

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 : )

seefeldb and others added 7 commits December 9, 2025 15:06
* feat(memory): Incremental schema subscription updates

Optimize getSchemaSubscriptionMatches by preserving the schemaTracker
between invocations instead of re-running full querySchema for every
subscription after each commit.

Changes:
- Extend SchemaSubscription to store schemaTracker (doc→schema mappings)
- Add evaluateDocumentLinks() for single-doc schema evaluation
- Add querySchemaWithTracker() to expose schemaTracker from initial query
- Replace full re-query with incremental update:
  1. Find changed docs that exist in subscription's schemaTracker
  2. Re-evaluate only those docs with their associated schemas
  3. Follow new links incrementally (not already in schemaTracker)
  4. Accumulate only new/changed facts for the response

This significantly reduces work for subscriptions with large result sets
where commits typically only affect a small portion of tracked documents.
Falls back to full re-query on errors for safety.

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

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

* fmt

* fix(memory): Use correct key format for schemaTracker lookup

The schemaTracker uses "id/type" format (from BaseObjectManager.toKey),
but extractChangedDocKeys was using "id\0type" format with null separator.
This caused changed docs to never match entries in schemaTracker, breaking
incremental subscription updates.

Also use lastIndexOf('/') instead of split to handle IDs containing slashes.

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

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

* fix(memory): Track docs in schemaTracker even without schemaContext

When schemaContext is undefined, the document was being loaded into the
manager but not added to schemaTracker. This caused incremental updates
to miss changes to these documents since they wouldn't be found in
findAffectedDocs.

Now we always add the document to schemaTracker with its selector,
ensuring all documents in the query result can be tracked for changes.

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

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

* fix(memory): Add fallback when schemaTracker misses watchedObjects match

The incremental update optimization now uses a two-phase approach:
1. First check if Subscription.match() triggers on watchedObjects
2. Then try to use schemaTracker for incremental processing
3. Fall back to full re-query for subscriptions where watchedObjects
   matches but schemaTracker doesn't have the changed docs

This is more complex than ideal - the root cause is that schemaTracker
and watchedObjects can get out of sync because they're populated via
different code paths and use different key formats:
- watchedObjects: watch:///${space}/${of}/${the}
- schemaTracker: ${of}/${the}

TODO: Unify these tracking mechanisms so watchedObjects becomes
redundant and can be removed.

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

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

* fix(memory): Use indexOf instead of lastIndexOf to parse docKey

The docKey format is "id/type" where type can contain slashes
(e.g., "application/json"). Using lastIndexOf("/") incorrectly
split "of:HASH/application/json" into:
- docId: "of:HASH/application" (wrong)
- docType: "json" (wrong)

Using indexOf("/") correctly splits at the first slash:
- docId: "of:HASH" (correct)
- docType: "application/json" (correct)

This was causing selectFact to return null, so incremental updates
never returned any facts, breaking subscription notifications.

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

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

* refactor(memory): Simplify schema subscription matching

Replace dual tracking (watchedObjects + schemaTracker) with clearer separation:

- schemaTracker: tracks which docs to watch with which schemas
- sentDocs: tracks which docs have been sent to the client
- isWildcardQuery: flag for queries with of: "_"

For wildcard queries, match changed docs by type pattern instead of
re-running the full query. Both wildcard and non-wildcard queries now
use the same incremental processing flow.

Remove fallback re-query code paths - throw on mount error instead.

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

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

* fix(memory): Add cycle detection for growing path cycles in incremental updates

Track per-document visit counts to detect cycles like A -> A/foo -> A/foo/foo
that create infinitely growing paths. Limits each document to 100 visits
before logging a warning and stopping further traversal.

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
…rage locations (#2235)

* feat(runner): auto-start charms when events are sent to unhandled storage locations

When an event is sent to a storage location that has no registered event handler,
the scheduler now attempts to start the underlying charm. This enables lazy charm
initialization - charms can be started on-demand when they first receive an event.

The implementation:
- Adds ensureCharmRunning() utility that traverses the source cell chain to find
  the process cell, then starts the charm via runtime.runSynced()
- Modifies queueEvent() to call ensureCharmRunning() when no handler is found
- Includes infinite loop protection to prevent re-queuing if the charm doesn't
  register a handler for the event

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

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

* test(runner): add tests for ensureCharmRunning and auto-start behavior

Tests cover:
- Return false for cells without process cell structure
- Return false for cells without TYPE in process cell
- Return false for cells without resultRef in process cell
- Successfully start charm with valid process cell structure
- Infinite loop protection (don't attempt to start twice)
- Graceful handling of events for orphan cells
- No infinite retry when charm doesn't register handler

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

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

* fmt

* test: improved tests for ensureCharmRunning

- Adds test proving charm starts when event is sent to a cell with no handler
- Adds test proving handler is called when defined for the stream path
- Tests verify infinite loop protection (charm only starts once per path)
- Updates orphan cell test to check actual values instead of expect(true)

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

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

* refactor(runner): simplify ensureCharmRunning by removing tracking

Remove the startAttemptedForCell Set and cellLinkKey helper since
runtime.runSynced() is already idempotent for running charms - calling
it multiple times simply returns without doing anything.

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

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

* fix(runner): prevent infinite loop in queueEvent when no handler exists

When re-queuing an event after starting a charm, pass a flag to prevent
triggering another charm load attempt. This avoids infinite loops when
the charm doesn't register a handler for that event type.

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

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

* test(runner): add test for restarting stopped charms

Verifies that ensureCharmRunning properly restarts a charm that was
previously stopped via runtime.runner.stop().

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
#2237)

* fix(runner): capture reactivity log after callback to track all cell reads

  In subscribeToReferencedDocs(), the reactivity log was being captured
  BEFORE the callback executed. This meant that any cell reads occurring
  during the callback - such as accessing properties of derived arrays
  during JSX rendering - were not tracked for subscription purposes.

  The symptom: styles referencing derived data (e.g., 'derive()' output
  used for dynamic background colors) would render correctly on initial
  load but never update when the underlying cell changed. The handler
  would fire, the derive would recompute, but the UI wouldn't re-render.

  Root cause: When rendering JSX like:
    background: filteredItemsWithHighlight[index]?.highlightBg

  The array element access happens during the effect callback. Since
  txToReactivityLog(tx) was called before callback(value), these reads
  weren't captured in the subscription, so changes to the derived cell's
  internal data didn't trigger the effect to re-run.

  The fix moves txToReactivityLog(tx) to after callback(value), ensuring
  all cell reads during rendering are captured and subscribed to.

* add regression test
* initial draft implementation of action

* fix Handler/HandlerFactory types to return Stream instead of OpaqueRef

- Handler.with and HandlerFactory now correctly typed to return Stream<R>
- stream() function in opaque-ref.ts now returns Stream<T>
- ActionFunction now returns HandlerFactory<T, void>
- Schema generator detects Stream in callable return types for result schema

This makes the types accurately reflect runtime behavior where handlers
return Streams. Fixes action() result schema generation.

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

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

* add runtime tests for action, fix missing { proxy: true } option
…-session (#2241)

Previously, each SchemaSubscription tracked its own `sentDocs` Set and
`since` value to avoid sending duplicate documents. This was suboptimal
because the same document could be sent multiple times on the same
WebSocket session if requested by different subscriptions.

Now we use the session-level `lastRevision` Map (which already existed
for non-schema subscriptions) to track which documents have been sent
and at what `since` value. This ensures that each document is only sent
once per session, regardless of how many subscriptions request it.

Changes:
- Remove `since` and `sentDocs` fields from SchemaSubscription class
- Simplify addSchemaSubscription to not track sent docs
- Update getSchemaSubscriptionMatches to use session-level lastRevision
- Let filterKnownFacts handle updating lastRevision when facts are sent

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

Co-authored-by: Claude <noreply@anthropic.com>
#2238)

The previous approach used `T extends AnyBrandedCell<any>` (without tuple
wrapper) to handle intersection types like `OpaqueCell<X> & Y`. However,
this caused TypeScript to distribute over union types, which broke the
null-preservation fix from commit 3f3de55.

For example, `string | null extends AnyBrandedCell<any>` distributes to:
- `string extends AnyBrandedCell<any>` (evaluated separately)
- `null extends AnyBrandedCell<any>` (evaluated separately)

This changed the type structure and caused schema generation to lose null.

The correct approach handles nullable intersection types in the nullable
handling section of OpaqueRefInner:
  `[NonNullable<T>] extends [AnyBrandedCell<any>] ? T`

This:
1. Uses the tuple wrapper to prevent distribution
2. Strips null/undefined first with NonNullable
3. Then checks if the remaining type is a branded cell

For `(OpaqueCell<X> & X) | undefined`:
- NonNullable gives `OpaqueCell<X> & X`
- Tuple check correctly identifies it as a branded cell
- Returns T unchanged (preserving the union with undefined)

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

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
@pull pull bot locked and limited conversation to collaborators Dec 10, 2025
@pull pull bot added the ⤵️ pull label Dec 10, 2025
@pull pull bot merged commit a5237e7 into ExaDev:main Dec 10, 2025
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.

3 participants