Skip to content

Nexus: raw ActivityClient starts inherit request ID and links - #2369

Open
tekkaya wants to merge 1 commit into
mainfrom
nexus-saa-gap-fixes
Open

tekkaya wants to merge 1 commit into
mainfrom
nexus-saa-gap-fixes

Conversation

@tekkaya

@tekkaya tekkaya commented Sep 1, 2026 •

Copy link
Copy Markdown

Summary

TemporalNexusClient.startActivity() guards the one Activity start that can complete a Nexus operation, but a synchronous handler that starts additional Activities must call the raw ActivityClient. Those starts previously omitted the inbound Nexus request ID and links, so a Nexus-task redelivery could start a duplicate Activity instead of resolving to its original run.

Raw Activity starts now inherit the handler’s request ID and inbound links. Completion callbacks remain limited to the guarded start, since only that start can complete the operation.

Aligns with the sdk-go PR #2633 and sdk-java PR #3048.

Changes

  • Add an internal ambient Nexus Activity-start context for the duration of a startOperation handler invocation.
  • Make raw ActivityClient starts inherit the inbound Nexus request ID and links.
  • Forward server-provided Activity response links to the operation’s outbound links.
  • Leave guarded-start completion-callback behavior unchanged.

Testing

  • Added a redelivery test showing that a raw Activity start runs exactly once after a retryable handler failure.
  • Added link-propagation coverage for standalone Nexus and Workflow callers.
  • Existing multiple-start and retry coverage continues to pass.

@tekkaya
tekkaya force-pushed the nexus-saa-gap-fixes branch 4 times, most recently from 4d0af10 to 5eb639f Compare September 1, 2026 04:45
@tekkaya
tekkaya marked this pull request as ready for review September 1, 2026 16:12
@tekkaya
tekkaya requested review from a team as code owners September 1, 2026 16:12
@tekkaya

tekkaya commented Sep 1, 2026

Copy link
Copy Markdown
Author

Note: Cloud Integ Tests seems like failing since PR #2367 is merged

* @internal
* @hidden
*/
export interface NexusActivityStartContext {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This shouldn't be specific to Activity start. This is the context around a StartNexusOperation task and should reflect that rather than being activity specific.

Let's also move it to packages/nexus/context.ts

@tekkaya tekkaya Sep 1, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

StartNexusOperationTaskContext or NexusOperationTaskContext or NexusStartOperationTaskContext any other suggestions for the naming? I am leaning towards NexusStartOperationTaskContext to align with NexusStartOperationInput existing NexusCancelOperationInputtypes

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Let's also move it to /packages/nexus/context.ts

nexus package already depends on client, so moving this into packages/nexus/context.ts and having ActivityClient read it back would create a real circular package dependency. Kept it in client, exported only through internal.ts, for that reason.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thinking about the name made me realize we actually already have a type with these exact fields that is passed to all handlers: TemporalStartOperationContext. This is the type that should be used I think.

Comment thread packages/client/src/nexus-activity-start-context.ts Outdated
Comment thread packages/client/src/activity-client.ts Outdated
Comment thread packages/client/src/activity-client.ts Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's take a look at making these changes via an internal ActivityClientInterceptor that is appended at the end of the interceptor chain (so user interceptors run first). That way we can encapsulate this Nexus context specific changes away from this general client implementation. The interceptor can add or modify the internal options and then hopefully we won't need any changes here.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That makes a lot of sense. I'll go with that direction.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Updated ptal @VegetarianOrc

@tekkaya
tekkaya force-pushed the nexus-saa-gap-fixes branch 3 times, most recently from 6e78a9a to ba0a222 Compare September 2, 2026 16:19
Comment thread packages/nexus/src/workflow-helpers.ts Outdated
Comment on lines +652 to +660
if (this._client == null) {
const base = getClient();
const activity = new ActivityClient({
...base.activity.options,
interceptors: [...(base.activity.options.interceptors ?? []), this.nexusActivityStartInterceptor],
});
this._client = Object.assign(Object.create(base), { activity }) as Client;
}
return this._client;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rather than applying the interceptor here, I think we should apply the interceptor on client creation. Doing it here allows users to avoid it by calling getClient() directly. The interceptor should no-op if not in a Nexus context so it should be safe to invoke each time. We'll also need to apply the same type of interceptor to the various workflow integration points (start, signal, update, etc). If you'd like to keep this PR focused only on Activities, that's fine let's just make sure we create an issue to track.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I would like to keep this focused on Nexus + SAA and can create an issue to track Nexus + start,signal,update

@tekkaya
tekkaya force-pushed the nexus-saa-gap-fixes branch 3 times, most recently from c7ec68b to 3efd787 Compare September 11, 2026 19:03

@VegetarianOrc VegetarianOrc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this looks good to me now, ty for all the updates!

Would love @chris-olszewski or @mjameswh to take a final look before merging.

@chris-olszewski chris-olszewski left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The interceptor is a nice way to provide this behavior. Nonblocking, but a nice follow up would be asserting how this is displayed in the stack trace. (Along with injecting this interceptor at creation as was mentioned by @VegetarianOrc)

`TemporalNexusClient.startActivity()` guards the one Activity start that can
complete a Nexus operation, but a handler that starts additional Activities
must call the raw client directly -- either `nexusClient.client.activity.start(...)`
or the module-level `getClient()` export. Neither of those raw starts
inherited the inbound Nexus request ID or links, so a Nexus-task redelivery
could start a duplicate Activity instead of resolving to its original run.

Activities started through either raw path now inherit the handler's request
ID and inbound links. When the server returns an Activity response link, it
is added to the operation's outbound links. Completion callbacks remain
limited to the guarded start, since only that start can complete the
operation.

Implemented as `nexusActivityStartInterceptor`, an `ActivityClientInterceptor`
installed once on the Worker's Client (`compileWorkerInterceptors`), gated by
an ambient per-task context so it is a no-op outside an active Nexus
operation. Because it is installed once at Client construction rather than
reconstructed per-invocation, it applies uniformly whether an Activity is
started by the operation handler itself or by an inbound interceptor, and
whether reached via `nexusClient.client` or the plain `getClient()` export.

Aligns with sdk-go #2633 and sdk-java #3048.
@tekkaya
tekkaya force-pushed the nexus-saa-gap-fixes branch from 82c56f7 to 507270c Compare September 17, 2026 04:23

This branch has not been deployed

No deployments
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.

3 participants