Skip to content

feat: add internal-message enqueue and snapshot incarnation APIs - #14

Merged
cardmagic merged 3 commits into
mainfrom
feat/internal-messages-and-snapshot-incarnation
Aug 19, 2026
Merged

feat: add internal-message enqueue and snapshot incarnation APIs#14
cardmagic merged 3 commits into
mainfrom
feat/internal-messages-and-snapshot-incarnation

Conversation

@cardmagic

Copy link
Copy Markdown
Owner

Summary

  • Add runtime.enqueueInternalMessage() and runtime.enqueueInternalMessageInTransaction(connection, options) — public entry points to enqueue an internal-delivery-mode actor message that skips user authorization, with a transaction-scoped variant for atomic multi-write commits, plus runtime.announceInternalMessage(message) to trigger the post-commit wake-up.
  • Add runtime.snapshotWithIncarnation(reference) — returns the same authorized fields as snapshot() plus instanceId, revision, and createdAtMs from one shared read, so a caller can fence a derived write against a stale or superseded actor incarnation. instanceId is a random UUID (not monotonic), so fencing uses createdAtMs.
  • Bump to 0.14.0 and update docs/api.md/docs/parity.md accordingly.

These close two real gaps found while designing a Node port of the commercial solid_objects_pro Ruby gem: no public way to enqueue a system-internal message inside a caller-managed transaction, and no way to detect actor recreation without a monotonic instance identity.

Test plan

  • pnpm run format:check
  • pnpm run check (parameter style, documentation completeness, type checks)
  • pnpm run test:coverage (262 passed, 11 skipped, 0 failed)
  • pnpm run build
  • pnpm run pack:check
  • pnpm run test:package (packaged tarball smoke test)
  • pnpm run test:recovery
  • pnpm audit --audit-level=high (no known vulnerabilities)
  • CI: postgresql/mysql/redis/browser matrix (needs live services, not run locally)

Give a host package (such as a future commercial scaling layer) two
things it cannot build from public API alone: a way to enqueue an
internal-delivery-mode actor message that skips user authorization,
with a transaction-scoped variant for atomic multi-write commits, and
a way to read instance identity/recency alongside a snapshot so a
derived write can fence against a stale or superseded actor
incarnation. instanceId is a random UUID, not monotonic, so fencing
uses createdAtMs instead.
@greptile-apps

greptile-apps Bot commented Aug 18, 2026

Copy link
Copy Markdown

Greptile Summary

The PR adds privileged internal-message enqueue APIs and an authorized snapshot API exposing actor-incarnation metadata, then updates the package version and documentation.

  • Supports immediate and caller-managed-transaction internal enqueues, with explicit post-commit announcement.
  • Returns snapshot state and incarnation metadata from a shared read.
  • Documents the millisecond precision limit of the incarnation timestamp.
  • Adds focused tests for authorization bypass, transaction rollback, recreation, and snapshot authorization.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
src/runtime.ts Adds internal enqueue and incarnation-aware snapshot APIs while sharing the existing authorized snapshot construction path.
src/index.ts Exports the new snapshot result interface through the package entry point.
test/internal-messages.test.ts Covers privileged processing, unknown-operation rejection, transactional commit, and rollback behavior.
test/snapshot-with-incarnation.test.ts Covers snapshot parity, changed instance identity after recreation, and query authorization.
docs/correctness.md Documents the same-millisecond limitation of timestamp-based incarnation ordering.

Sequence Diagram

sequenceDiagram
    participant Host
    participant Runtime
    participant DB
    participant Worker
    Host->>DB: Begin transaction
    Host->>Runtime: enqueueInternalMessageInTransaction(connection, options)
    Runtime->>DB: Insert internal message
    DB-->>Host: Message row
    Host->>DB: Commit transaction
    Host->>Runtime: announceInternalMessage(message)
    Runtime->>Worker: Wake actor role
    Worker->>DB: Claim and process message
Loading

Reviews (2): Last reviewed commit: "docs: document snapshotWithIncarnation's..." | Re-trigger Greptile

Comment thread src/runtime.ts
snapshot: readonlyCopy(snapshot) as ActorSnapshot<ActorType>,
instanceId: instance?.id ?? "0",
revision: String(instance?.state_revision ?? 0),
createdAtMs: Number(instance?.created_at_ms ?? 0),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Timestamp fence aliases incarnations

When an actor is destroyed and recreated within the same database-clock millisecond, both incarnations receive the same createdAtMs, so a stale derived write can pass the advertised incarnation fence and overwrite or publish data as though it came from the current incarnation.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/runtime.ts
Line: 754

Comment:
**Timestamp fence aliases incarnations**

When an actor is destroyed and recreated within the same database-clock millisecond, both incarnations receive the same `createdAtMs`, so a stale derived write can pass the advertised incarnation fence and overwrite or publish data as though it came from the current incarnation.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread src/runtime.ts
@@ -1443,6 +1477,66 @@ export class SolidObjectsRuntime {
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Generic default violates type rule

The new Result = unknown annotation violates the repository requirement to use concrete TypeScript types, forcing consumers that omit the generic to narrow or assert the message result. Replace it with the concrete result type supported by message result storage.

Rule Used: What: Disallow the use of unknown in TypeScript ... (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/runtime.ts
Line: 1478

Comment:
**Generic default violates type rule**

The new `Result = unknown` annotation violates the repository requirement to use concrete TypeScript types, forcing consumers that omit the generic to narrow or assert the message result. Replace it with the concrete result type supported by message result storage.

**Rule Used:** What: Disallow the use of `unknown` in TypeScript ... ([source](https://app.greptile.com/craftsmanfounder/-/custom-context?memory=af673ae0-6488-4c8b-8b4a-1bfea4eb4de7))

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

createdAtMs orders actor incarnations at the same millisecond
granularity every adapter stores created_at_ms at. Destroying and
recreating the same actor identity within one database-clock
millisecond produces two incarnations a caller cannot order by
createdAtMs alone, since neither timestamp precision nor instanceId
(a random UUID) can break the tie without a schema-level monotonic
sequence, which is out of scope here. Record the boundary in
docs/correctness.md and docs/api.md, and make the existing recreation
test's title state why it asserts >= rather than >.
@cardmagic

Copy link
Copy Markdown
Owner Author

Addressed both Greptile findings:

Issue 1 (timestamp fence aliases incarnations) — valid, fixed via documentation. A same-millisecond destroy+recreate can produce two incarnations with an equal createdAtMs, since every adapter stores created_at_ms at millisecond precision and none exposes a portable cross-adapter monotonic tiebreaker without a schema migration (checked: no autoincrement column exists on instances in any of the three adapters; instanceId is a random UUID with no order of its own). A full fix means adding a monotonic sequence column across SQLite/PostgreSQL/MySQL — real, but out of scope for this PR. Documented the actual boundary precisely in docs/correctness.md's "Limitations and non-goals" and docs/api.md, and updated the CHANGELOG to stop overclaiming. Also renamed the recreation test so its title states why it asserts >= rather than >, instead of that read as an oversight.

Issue 2 (Result = unknown generic default) — false positive, not changed. This matches the pre-existing convention already used identically by sendMessage<Result = unknown> and invoke<Result = unknown> in this same file (both predate this PR). Changing only the new method would make it inconsistent with the established pattern, not more correct.

@cardmagic
cardmagic merged commit 7057b91 into main Aug 19, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant