Skip to content

Enforce promise id format origin:lineage, remove RESONATE_PREFIX - #12

Merged
Tomperez98 merged 2 commits into
mainfrom
id-format-origin-lineage
Aug 24, 2026
Merged

Tomperez98 merged 2 commits into
mainfrom
id-format-origin-lineage

Conversation

@Tomperez98

Copy link
Copy Markdown
Member

Ports the promise id format change to the Java SDK, mirroring resonate-sdk-py ec67529 (see also the server's new promise id rules, resonatehq/resonate#1127, and its colon_in_origin guard, commit d6fae28).

Every promise id is now <origin>:<lineage>: the origin (before the first :) is the caller-supplied workflow id, and the lineage segments below it are .-separated:

root -> root:1 -> root:1.1 -> root:1.1.1

: and . are therefore reserved and rejected in caller-supplied root ids.

1. RESONATE_PREFIX support removed (breaking)

  • Resonate.builder().prefix(...) removed; the RESONATE_PREFIX env var is no longer read.
  • Ids are no longer rewritten to {prefix}:{id} — the id you pass to run / rpc / get / schedule is the id you get.
  • The resonate:prefix tag is no longer emitted on any promise.

2. Id minting follows the server's separator rule

New Ids.java: joinId (a bare root joins its first lineage segment with :, deeper ones with .), originOf (everything before the first :, mirroring the server's origin()), and validateRootId.

  • Context.java — child ids minted with joinId; the prefixId plumbing is gone (the origin now doubles as the id-generation anchor). ctx.detached mints {origin}:d{16hex} and keeps the parent's origin (the server requires every id in a lineage to extend its origin), declaring the origin as its resonate:parent; recursion stays bounded at one segment past the origin.
  • Core.java — the root context's origin comes from the resonate:origin tag, falling back to Ids.originOf(promise.id()) for tag-less promises (same derivation the server uses).
  • Resonate.java — top-level promise tags are now origin == branch == parent == id (no resonate:prefix).

3. : (and .) rejected in user-supplied ids

  • run, rpc, and schedule validate the id at the call site and throw InvalidIdError (new, Errors.java) instead of surfacing an opaque 400 from a background create. get is a lookup and deliberately does not validate (it accepts child ids like wf:1.2).
  • schedule templates the fired promise id as {{.id}}-{{.timestamp}} (previously .-joined): the server stamps the whole templated id as the fired promise's origin, so it must stay a single bare segment.

4. Durable sleep vs target-gated timeouts

Since server commit c8d7c7b the scheduler only times out promises carrying a resonate:target; a target-less ctx.sleep promise would never fire.

  • Context.java — ctx.sleep timer promises now carry a resonate:target; resonate:timer makes the deadline settle the promise resolved, which wakes the sleepers.
  • Core.java — the target also spawns a task dispatched immediately; a timer names no function, so Core drops a task for a still-pending timer (no fulfill, no suspend, no release) and short-circuits a settled timer's task before the TaskData decode.
  • Network.java — LocalNetwork now mirrors the server's invariant (only targeted promises are scheduled for timeout), so this class of bug fails locally instead of only against a real server.

Examples

  • structuredconcurrency, humanintheloop — child-id comments/lookups updated to {id}:1 shape.

Tests

  • IdFormatTest.java (new) — a direct port of the server's validate_promise_create_data / origin(); every promise the SDK creates is replayed through it, plus joinId / originOf / validateRootId, detached id shape, and call-site rejection tests.
  • CoreTest — timer-task tests (drop pending, short-circuit settled, deadline scheduled by the server).
  • NetworkTest — scheduler invariant: only targeted promises are timed out; resonate:timer settles resolved.
  • Prefix tests removed; child-id expectations updated to root:1 shape throughout.

Verification

  • ./gradlew test — all pass
  • ./gradlew spotlessCheck — clean

- Ids are <origin>:<lineage>: the caller-supplied root id is the origin,
  lineage segments below it are '.'-separated (root -> root:1 -> root:1.1).
- Remove RESONATE_PREFIX env var and Builder.prefix support; drop the
  resonate:prefix tag.
- Reject the reserved separators ':' and '.' in run/rpc/schedule ids at the
  call site (InvalidIdError); get is a lookup and does not validate.
- Detached ids are {origin}:d{16hex}, minted off the lineage origin (kept from
  the parent) with resonate:parent = origin, so recursion stays bounded and
  every id in a lineage shares one origin.
- Adapt ctx.sleep to the server's target-gated timeouts: timers carry a
  resonate:target, Core drops/short-circuits timer tasks, LocalNetwork
  mirrors the server's only-targeted-promises-time-out invariant.
- Schedule promise-id template is now {{.id}}-{{.timestamp}}.

Mirrors resonate-sdk-py ec67529; matches the server's id rules
(resonatehq/resonate 'new promise id', #1127) and the colon_in_origin guard
(server commit d6fae28).
'.' separates lineage segments *below* the origin, which are only ever read
once the origin has been split off at the first ':'. A dotted root id is
therefore unambiguous and round-trips intact:

    my.app.workflow -> my.app.workflow:1 -> my.app.workflow:1.1

Only ':' stays reserved: the origin is everything before an id's first ':',
so an origin holding one could never be split back out of any id
(colon_in_origin).

The server's dot_in_origin guard -- a leftover from when '.' was the only
separator -- is dropped alongside this (resonatehq/resonate src/types.rs);
colon_in_origin stays. The ported server rules in the id-format tests drop
their dot_in_origin assertion to match, and the workflow compliance test now
also replays every minted id under a dotted root.
@Tomperez98
Tomperez98 merged commit 6c3428d into main Aug 24, 2026
4 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