Skip to content

feat(sdk): add writeStamp for exact session-id stamping#426

Merged
willwashburn merged 1 commit into
mainfrom
feat/sdk-write-stamp-by-id
May 21, 2026
Merged

feat(sdk): add writeStamp for exact session-id stamping#426
willwashburn merged 1 commit into
mainfrom
feat/sdk-write-stamp-by-id

Conversation

@willwashburn
Copy link
Copy Markdown
Member

Summary

  • Adds writeStamp({ sessionId | messageId, enrichment, ts?, ledgerHome? }) to @relayburn/sdk and relayburn_sdk for launchers that know the session id up front — companion to writePendingStamp, skips the sidecar manifest matching path entirely
  • Rust exposes the verb both as a free function (relayburn_sdk::write_stamp) and a LedgerHandle::write_stamp method; the Node SDK gets a writeStamp async export wired through napi
  • Empty selectors reject via StampError::EmptySelector to preserve writePendingStamp's "no matched-everything stamps" guarantee

Why

External launchers that already control a Claude Code session id (via claude --session-id <uuid>) can stamp burn directly by selector instead of relying on the sidecar cwd + spawnerPid + spawnStartTs match. This removes a class of failure modes: cwd-rewrite races, orphan manifests on spawn failure, GC of pending stamps after 24h, and the lag between writing the manifest and burn ingest finding it.

The first consumer is a Pear ↔ relay ↔ burn integration that preallocates the UUID, injects --session-id into the spawn args, and calls writeStamp so the resulting session shows up in burn summary --tags spawner=pear with no ambiguity about which jsonl file is which session. That integration ships as a follow-up PR in pear and relay once this lands and @relayburn/sdk republishes.

What's in the change

The committed binding.cjs / binding.d.ts are intentionally left as their stub state — the napi-build workflow regenerates them in CI with the new writeStamp export present.

Test plan

  • cargo test -p relayburn-sdk stamp_verb — 3 Rust unit tests pass
  • cargo test --workspace — full Rust workspace green
  • cargo build -p relayburn-sdk-node — napi binding compiles
  • pnpm -F @relayburn/sdk run build:napi && RELAYBURN_SDK_NAPI_BUILT=1 pnpm -F @relayburn/sdk test — all 11 conformance tests pass (incl. 3 new writeStamp tests)
  • CI: napi-build workflow regenerates per-platform .node artifacts + binding.cjs / binding.d.ts with writeStamp export

🤖 Generated with Claude Code

Launchers that know the session id up front — for example, a Claude
launcher that preallocates `--session-id <uuid>` before spawn — can now
call `@relayburn/sdk` `writeStamp({ sessionId, enrichment })` to fold
enrichment straight onto the ledger by selector, skipping the
`writePendingStamp` sidecar manifest matching path entirely.

The companion `writePendingStamp` flow still serves harnesses that don't
expose a pre-spawn session id (Codex, OpenCode). `writeStamp` is the
more reliable path when the id is available: no cwd/pid race, no orphan
manifest if the spawn fails, and the stamp resolves immediately at
ingest time rather than after the launcher first writes a turn.

The verb is exposed as a free function and a `LedgerHandle` method in
the Rust SDK (`relayburn_sdk::write_stamp`, `LedgerHandle::write_stamp`)
plus a napi-bound `writeStamp` in `@relayburn/sdk`. Selectors are
either `sessionId` or `messageId`; empty selectors reject via
`StampError::EmptySelector` to keep `writePendingStamp`'s "no
matched-everything stamps" guarantee.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a new writeStamp verb to the Relayburn SDK that allows direct ledger writes using session ID or message ID selectors. The feature flows from Rust SDK core through an N-API binding layer to a Node.js async wrapper, with conformance tests validating the full stack.

Changes

writeStamp Feature

Layer / File(s) Summary
Rust SDK stamp verb implementation
crates/relayburn-sdk/src/stamp_verb.rs, crates/relayburn-sdk/src/lib.rs, CHANGELOG.md
WriteStampOptions struct with optional session_id/message_id selectors and required enrichment drives both a free write_stamp() function and LedgerHandle::write_stamp() method. Stamp construction auto-generates ISO-8601 timestamps when omitted and persists via ledger append. Unit tests validate selector and enrichment requirements. Module wiring re-exports stamp functionality from the SDK crate.
N-API binding layer
crates/relayburn-sdk-node/src/lib.rs
N-API WriteStampOptions and write_stamp() function validate input constraints (at least one selector, non-empty enrichment with non-empty keys), map to SDK option types, dispatch to sdk::write_stamp, and map SDK errors to BurnError.
Node.js API surface and conformance tests
packages/sdk-node/src/index.d.ts, packages/sdk-node/src/index.js, packages/sdk-node/CHANGELOG.md, packages/sdk-node/test/conformance.test.js
TypeScript WriteStampOptions interface and async writeStamp(opts) wrapper expose the binding to JavaScript without BigInt coercion. SDK-node CHANGELOG documents the new API as a session-id-aware companion to writePendingStamp. Conformance tests verify export presence, enrichment persistence/round-trip, and input validation (empty selector rejection, empty enrichment rejection).

Sequence Diagram

sequenceDiagram
  participant Launcher as Launcher/Caller
  participant write_stamp_fn as write_stamp Function
  participant StampBuilder as Stamp Builder
  participant Ledger
  Launcher->>write_stamp_fn: WriteStampOptions{sessionId, enrichment}
  write_stamp_fn->>StampBuilder: convert selector and enrichment
  StampBuilder->>StampBuilder: auto-generate ISO timestamp if omitted
  StampBuilder->>Ledger: Stamp::new
  write_stamp_fn->>Ledger: append stamp
  Ledger-->>write_stamp_fn: persisted
  write_stamp_fn-->>Launcher: Result<()>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • AgentWorkforce/burn#355: Modifies the conformance test harness that this PR's writeStamp tests depend on for ledger setup and seed configuration.

Poem

🐰 A stamp upon the ledger true,
With session-id to guide it through,
No sidecar dance, no pending way—
Direct writes brighten every day!
Enrichment flows where selectors lead,
The SDK fulfills our need! 🔖

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: adding a writeStamp API for stamping sessions by exact ID, which is the primary feature across all modified files.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the motivation, implementation details across Rust and Node layers, testing, and the use case for launchers with preallocated session IDs.
Docstring Coverage ✅ Passed Docstring coverage is 92.31% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sdk-write-stamp-by-id

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
crates/relayburn-sdk/src/stamp_verb.rs (3)

26-30: ⚡ Quick win

Consider removing Default derive or adding enrichment validation.

WriteStampOptions derives Default with a non-optional enrichment field. This allows callers to construct an instance with empty enrichment via WriteStampOptions::default(), which the PR description says should be rejected. However, validation only occurs at the N-API boundary (line 521-523 in crates/relayburn-sdk-node/src/lib.rs), not in the Rust SDK layer.

Consider either:

  • Removing #[derive(Default)] so invalid states cannot be constructed
  • Adding validation in build_stamp or write_stamp to reject empty enrichment
  • Documenting that validation is the caller's responsibility
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/relayburn-sdk/src/stamp_verb.rs` around lines 26 - 30,
WriteStampOptions currently derives Default even though enrichment is required,
allowing invalid empty-enrichment instances; remove #[derive(Default)] from
WriteStampOptions to prevent constructing default/invalid states, and/or add a
runtime check in build_stamp and/or write_stamp (e.g., validate enrichment is
non-empty at the start of those functions) that returns an error if enrichment
is empty; update any callers/tests that relied on WriteStampOptions::default()
or handle the new error path accordingly.

71-82: ⚡ Quick win

Timestamp conversion truncates subsecond precision and silently falls back on errors.

The now_iso function has two issues:

  1. Precision loss (lines 72-75): Converting SystemTime to seconds via duration_since(UNIX_EPOCH).as_secs() truncates milliseconds/nanoseconds. The N-API layer's parse_iso_system_time preserves fractional seconds (lines 563-570), creating an asymmetry.

  2. Silent error handling (lines 72-77): Both duration_since and from_unix_timestamp failures use unwrap_or to fall back to 0 or UNIX_EPOCH, hiding potential clock issues.

If subsecond precision is not needed for stamps, document this intentionally. Otherwise, preserve milliseconds using time::Duration::milliseconds() or similar. For error handling, consider propagating errors instead of silent fallback.


89-103: 💤 Low value

Test validates empty enrichment rejection, but Rust SDK doesn't enforce it.

The test empty_selector_is_rejected (lines 89-103) uses enrichment: BTreeMap::new() (line 94), which is empty. However, the test only verifies selector validation; it doesn't test enrichment validation. Per the PR description, empty enrichment should also be rejected, but this validation only exists at the N-API boundary.

Consider adding a test case that verifies rejection of empty enrichment at the Rust SDK level if validation is added per the previous comment, or clarify in documentation that enrichment validation is deferred to higher layers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/relayburn-sdk/src/stamp_verb.rs` around lines 89 - 103, The test
points out empty enrichment maps are currently allowed at the Rust SDK level;
add a validation step in the write_stamp function (or in the
constructor/validation for WriteStampOptions) to reject an empty enrichment
BTreeMap by returning an error when enrichment.is_empty() is true, and update or
add a unit test mirroring empty_selector_is_rejected that calls write_stamp with
enrichment: BTreeMap::new() and asserts the error contains "enrichment" so empty
enrichment is enforced in the Rust SDK rather than only at the N-API boundary.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 9-11: Update the changelog entry to mention both SDK surfaces: add
a line for the Rust symbol relayburn_sdk::write_stamp and the Node export
`@relayburn/sdk.writeStamp` (or combine into one line stating the feature exists
in both), and replace the implementation-detail phrase "bypassing the sidecar
`writePendingStamp` matching path" with a concise user-facing description such
as "skipping manifest matching" or "writes enrichment immediately using the
known session id" to align with existing entries like the `relayburn-sdk:`
prefix style.

---

Nitpick comments:
In `@crates/relayburn-sdk/src/stamp_verb.rs`:
- Around line 26-30: WriteStampOptions currently derives Default even though
enrichment is required, allowing invalid empty-enrichment instances; remove
#[derive(Default)] from WriteStampOptions to prevent constructing
default/invalid states, and/or add a runtime check in build_stamp and/or
write_stamp (e.g., validate enrichment is non-empty at the start of those
functions) that returns an error if enrichment is empty; update any
callers/tests that relied on WriteStampOptions::default() or handle the new
error path accordingly.
- Around line 89-103: The test points out empty enrichment maps are currently
allowed at the Rust SDK level; add a validation step in the write_stamp function
(or in the constructor/validation for WriteStampOptions) to reject an empty
enrichment BTreeMap by returning an error when enrichment.is_empty() is true,
and update or add a unit test mirroring empty_selector_is_rejected that calls
write_stamp with enrichment: BTreeMap::new() and asserts the error contains
"enrichment" so empty enrichment is enforced in the Rust SDK rather than only at
the N-API boundary.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 048bec1b-db49-4c17-8677-fcf3f9e7bac4

📥 Commits

Reviewing files that changed from the base of the PR and between 34f2e3b and 1bae2ba.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • crates/relayburn-sdk-node/src/lib.rs
  • crates/relayburn-sdk/src/lib.rs
  • crates/relayburn-sdk/src/stamp_verb.rs
  • packages/sdk-node/CHANGELOG.md
  • packages/sdk-node/src/index.d.ts
  • packages/sdk-node/src/index.js
  • packages/sdk-node/test/conformance.test.js

Comment thread CHANGELOG.md
Comment on lines +9 to +11
- `@relayburn/sdk`: `writeStamp({ sessionId | messageId, enrichment })` for
launchers that know the session id up front (e.g. preallocated Claude
`--session-id`), bypassing the sidecar `writePendingStamp` matching path.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Changelog entry should mention both Rust and Node SDK surfaces.

The entry documents @relayburn/sdk (Node package) but omits the Rust SDK surface. Per the PR objectives, this feature adds relayburn_sdk::write_stamp (Rust) and @relayburn/sdk.writeStamp (Node). Looking at other entries in this changelog (e.g., lines 15-20, 26-29), Rust SDK changes are documented with the relayburn-sdk: prefix.

Consider splitting or expanding the entry:

- `relayburn-sdk` (Rust): `write_stamp({ sessionId | messageId, enrichment })` 
  for direct ledger writes by exact selector.
- `@relayburn/sdk` (Node): `writeStamp` export wraps the Rust SDK verb.

Or combine them:

- `writeStamp({ sessionId | messageId, enrichment })` in both Rust SDK 
  and `@relayburn/sdk` (Node) for launchers that know session id up front 
  (e.g. preallocated Claude `--session-id`).

As per coding guidelines, the phrase "bypassing the sidecar writePendingStamp matching path" leans toward implementation detail. Consider tightening to: "skipping manifest matching" or focusing on the practical benefit: "writes enrichment immediately using the known session id."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 9 - 11, Update the changelog entry to mention both
SDK surfaces: add a line for the Rust symbol relayburn_sdk::write_stamp and the
Node export `@relayburn/sdk.writeStamp` (or combine into one line stating the
feature exists in both), and replace the implementation-detail phrase "bypassing
the sidecar `writePendingStamp` matching path" with a concise user-facing
description such as "skipping manifest matching" or "writes enrichment
immediately using the known session id" to align with existing entries like the
`relayburn-sdk:` prefix style.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1bae2bad9e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +78 to +80
let fmt = time::macros::format_description!(
"[year]-[month]-[day]T[hour]:[minute]:[second]Z"
);
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 Badge Keep default stamp timestamps in ms ISO format

write_stamp now defaults ts to YYYY-MM-DDTHH:MM:SSZ, but existing stamp writers use millisecond ISO strings (...SS.mmmZ). Stamp application order is read from SQLite as text-ordered ORDER BY ts, written_at (collect_stamps in crates/relayburn-sdk/src/ledger/reader.rs), so mixing second-only and millisecond formats can invert precedence for stamps in the same second (e.g. .500Z sorts before Z). That can cause later enrichment to be overwritten by older data.

Useful? React with 👍 / 👎.

Comment on lines +516 to +519
if opts.session_id.is_none() && opts.message_id.is_none() {
return Err(invalid_arg(
"writeStamp requires at least one of sessionId or messageId",
));
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 Badge Reject empty selector strings for writeStamp

The argument guard only checks whether sessionId/messageId are None, so "" passes validation and is written as a selector. Because matching is exact, an empty id will usually match no turns, producing a successful-but-noop stamp row that is hard for callers to detect. This is especially easy when values come from env vars/flags that may be present but empty.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 4 additional findings.

Open in Devin Review

@willwashburn willwashburn merged commit 0a8f598 into main May 21, 2026
13 checks passed
@willwashburn willwashburn deleted the feat/sdk-write-stamp-by-id branch May 21, 2026 14:56
willwashburn pushed a commit that referenced this pull request May 21, 2026
….7/2.8.6

Main released 2.9.0, 2.8.7, and 2.8.6 (#421, #422, #423, #426). The
auto-merge against ingest.rs and lib.rs was clean (no behavior
conflict with #423's walk dedupe + run_single_harness refactor); only
CHANGELOG needed manual sorting to keep this branch's items under
[Unreleased] above the new release sections.

https://claude.ai/code/session_011ubB69Zxijqb1BsYVYL9iQ
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