Skip to content

fix(start-os,start-sdk): run effect-initiated actions under the caller's eventId - #4069

Merged
dr-bonez merged 4 commits into
masterfrom
fix/effects-event-id
Sep 24, 2026
Merged

dr-bonez merged 4 commits into
masterfrom
fix/effects-event-id

Conversation

@helix-a

@helix-a helix-a commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

What changes

One name for the calling procedure's id: eventId, from the runtime's envelope to the service actor.

  • service/effects/prelude.rs: the unused EventId struct is now the single owner of the caller's event id. It is a serde field (eventId, as the runtime sends it) and a clap arg (--event-id). The three action effect params (GetActionInputParams, RunActionParams, CreateTaskParams) flatten it in place of their procedure_id fields, and EventId::or_new resolves it for the handlers. Two details:
    • The field is an Option<Guid>, so neither clap nor the man pages carry a random default.
    • Its comment is a plain //: as a doc comment, clap takes it as the about text of every command that flattens it.
  • service/action.rs, service/mod.rs, service/persistent_container.rs: procedure_id / id are renamed to event_id wherever they carry this id, down to the runtime's execute.
  • feat(start-sdk,start-os)!: let a service run an action that takes input #4062's separate eventId on the effect is folded into this one. For a service, Effects.action.run and sdk.action.run pass nothing; the runtime supplies the id. The in-container CLI keeps start-container action run --event-id, so get-input → run --event-id answers a form from a shell. A command takes the flag when it continues an event an earlier call started; get-input starts one and returns its id, so it has none, as in start-cli package action. The man pages are unchanged from master. The EffectsRunActionParams binding loses eventId, and the unused EventId binding goes; no TypeScript imported either.
  • Docs: the packaging book's actions page and the unreleased 3.0.0 changelog now say the form and its run share the caller's procedure id.

Why

The container runtime sets eventId on every effect call to the calling procedure's id (EffectCreator.rpcRoundFor). The action effects declared the field as procedure_id, which serde reads from procedureId, so they never saw that id. Every effect-initiated get-input, run and task-input lookup reached the target's ConcurrentActor under a fresh random Guid.

That actor skips a message's conflicts with a running handler of the same id (util/actor/concurrent.rs, &id != hid). So the mismatch cost two things:

  • Re-entrancy: effect-initiated calls lost it. create_task already works around a self-call deadlock with try_get.
  • Forms: a getInput and the run answering it never shared an id.

#4062 patched the second with an explicit eventId on effects.action.run. On a VM running #4062's build that still failed: the runtime overwrote the field with the caller's own procedure id, and Tor refused Bitcoin's run with getActionInput has not been called for EventID …. #4068 tried to let that field win over the tag; it is closed in favour of this, per dr-bonez: read the id that is already sent, under the name it is sent under.

With this change, a form opened by effects.action.getInput and the effects.action.run answering it share the calling procedure's id, and nothing extra is passed. sdk.action.run keeps its shape: it opens the form, applies the input function and runs. Its constraint: the target keys a form by the caller's procedure id, so one procedure answers one form at a time. The helper and the book say so.

Verification

  • New Rust tests in service::effects::action::test:
    • Each of the three params structs, deserialized from an envelope, carries its eventId.
    • run parses --event-id and keeps it through serialization to the request; get-input rejects the flag.
    • Without the flag, no id is sent.
  • cargo test --features=test -p start-core --lib -- --skip export_: 587 pass. util::http_reader::main_test fails because it fetches https://docs.start9.com/llms.txt, which times out from the host I ran it on.
  • cargo check -p start-core and make start-core-format-check pass. make start-core-ts-bindings and make manpages change only the files listed above.
  • start-core: tsc passes and jest passes 17 suites. runAction.test.ts now models StartOS: every call from one procedure runs under that procedure's id. It checks that a run answers the form its own procedure opened, and that a run from another procedure is refused.
  • start-sdk: make bundle, make check and make test (131 tests) pass. container-runtime: npm run check and npm test pass.
  • Prettier 3.8.3 passes on the changed TypeScript and Markdown.
  • Not yet on a box. Next is Bitcoin → Tor on the same VM with this PR's CI build; I'll report the result here.

For the reviewer

Re-entrancy now works as the actor intends for effect-initiated calls. One consequence: two calls from one procedure into the same service no longer wait on each other's conflicts, because they share an id. That matches what the actor already does for calls from inside a handler. No StartOS changelog entry: nothing a user sees changes that I can point to.

…r's eventId

The container runtime tags every effect call with the calling procedure's
event id under `eventId`, but the action effects declared the field as
`procedure_id`, which serde reads from `procedureId`. So every
effect-initiated get-input, run and task lookup reached a service's actor
under a fresh random id. ConcurrentActor skips a message's conflicts with a
running handler of the same id, so those calls lost that re-entrancy, and a
getInput and the run answering it landed under different ids. That is why
#4062 added an explicit `eventId` to effects.action.run, and why it still
failed on a box: the runtime overwrote it with the caller's procedure id.

The three effect param structs now flatten one `EventId` (prelude.rs), the
single owner of the envelope field, and the service code names that id
`event_id` all the way to the runtime's `execute`. A form and the run
answering it now share the caller's procedure id with nothing passed, so
#4062's explicit `eventId` on effects.action.run and in sdk.action.run is
removed. The unused `EventId` binding goes with the old struct.

A unit test deserializes each params struct from an envelope and checks the
id arrives.

Helix-Harness: claude-code
Helix-Model: claude-opus-5-5
The previous commit flattened `EventId` into the effect params with
`#[arg(skip)]`, which removed `start-container action run --event-id`
(added in #4062). Without it every in-container CLI call gets a fresh id,
so `run` can never answer the form `get-input` opened.

`EventId` is now a clap arg group: `--event-id` on both `get-input` and
`run`, flattened after `--package-id`. Its field is an `Option<Guid>` so
neither clap nor the man pages carry a random default, and `EventId::or_new`
resolves it for the handlers. Its comment is a plain `//`: as a doc comment,
clap took it as the about text of every command that flattens it, replacing
the `get-input` and `run` descriptions.

Against master the man pages differ only by `--event-id` on `get-input`.
A new test parses `--event-id` for both commands and checks it survives
serialization to the request.

Helix-Harness: claude-code
Helix-Model: claude-opus-5-5
A CLI command takes an event id when it continues an event an earlier call
started: `run` answers the form `get-input` opened. `get-input` starts the
event and returns its id, so the flag has nothing to name there. This
matches `start-cli package action`, where only `run` has `--event-id`.
get-input keeps reading the envelope id; the man pages match master again.

Helix-Harness: claude-code
Helix-Model: claude-opus-5-5
@helix-a

helix-a commented Sep 23, 2026

Copy link
Copy Markdown
Member Author

End-to-end test on a VM, using this PR's CI build: compile run 35914236933, GIT_HASH a4a450a = f52c29d merged into master. f52c29d differs from the current head only in which CLI commands take --event-id, and the effects path is the same.

Setup

  • The VM started on StartOS 0.3.5.1 with Bitcoin and Tor installed, then took an OTA to 0.4.0.x and had this build installed through chroot-and-upgrade.
  • Bitcoin is 31.1:18 from bitcoin-core-startos#309, packed against SDK 3.0.0 built from this PR plus fix(start-sdk): drop undefined keys an onWrite transformer produces #4067. Its migration retires the 0.3.5 peer port 8333. Its init then opens Tor's Add Onion Service form and runs it through sdk.action.run for each unused peer .onion.
  • Tor is tor-startos#39 plus access: 'public'.

Before this build (the boot just before the upgrade, same packages), the run was refused:

action.run … "eventId":"JKHLKZKKR5BQBH7QLBHHXLQ34CASPYF5" … getActionInput has not been called for EventID E2Y236MU37WML3QDG5GVMAUT4BIESZ3N

With this build, the first boot's init did the reattach. Bitcoin's init ran as procedure YQJFAMLI444GWBVM5X6LYNF3WWEBCAX3, and Tor's log shows every call under that id:

{"id":"YQJFAMLI444GWBVM5X6LYNF3WWEBCAX3","procedure":"/actions/add-onion-service/getInput","input":{"prefill":{…"internalPort":58333}},"caller":"bitcoind"}
{"id":"YQJFAMLI444GWBVM5X6LYNF3WWEBCAX3","procedure":"/actions/add-onion-service/run","input":{"input":{…"address":{"selection":"bitcoind/peer/0","value":{}}},"caller":"bitcoind"}}

Resulting state

  • Tor store: bitcoind/peer/0 → {externalPort: 8333, internalPort: 58333}.
  • torrc: HiddenServicePort 8333 10.0.3.1:8333.
  • Bitcoin's 58333 binding lists fddsvz4q2zooldxdpsxpq37kvexlguf6wteouedn5jjt2tqczh6o75yd.onion:8333, which is the 0.3.5.1 address.
  • Bitcoin's store flag is cleared, and bitcoin.conf has no =undefined lines. On SDK 3.0 that needs fix(start-sdk): drop undefined keys an onWrite transformer produces #4067.

Traffic: a P2P version handshake to that .onion on 8333, sent through Tor's SOCKS port, got version 70016 ua /Satoshi:31.1.0/ back.

Comment thread shared-libs/crates/start-core/src/service/effects/prelude.rs Outdated
Helix-Harness: claude-code
Helix-Model: claude-opus-5-5
@dr-bonez
dr-bonez merged commit 6d9d1f9 into master Sep 24, 2026
32 checks passed
@dr-bonez
dr-bonez deleted the fix/effects-event-id branch September 24, 2026 20:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants