Skip to content

fix(local): show durable execution ARN as soon as the execution starts - #9205

Merged
roger-zhangg merged 6 commits into
aws:developfrom
ParidelPooya:fix/show-durable-execution-arn-at-start
Sep 2, 2026
Merged

roger-zhangg merged 6 commits into
aws:developfrom
ParidelPooya:fix/show-durable-execution-arn-at-start

Conversation

@ParidelPooya

@ParidelPooya ParidelPooya commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

sam local invoke printed the durable execution ARN only after the execution reached a terminal state, and otherwise logged it at DEBUG. That ARN is the only identifier sam local execution get, history and stop accept, so an execution that was still running -- suspended on a callback, waiting out a context.wait, or interrupted with Ctrl-C -- could not be inspected without re-running the invoke under --debug and reading it out of the log.

--durable-execution-name is not a way around it. The emulator builds the ARN from a pair of generated UUIDs and keeps the name only as metadata, so the execution commands answer "Execution not found" when given a name. Nothing else about a running execution is missing: GetDurableExecution and GetDurableExecutionHistory both return data mid-flight, so the ARN was the only gap.

The ARN is now printed as soon as start_durable_execution returns, before the wait begins. That mirrors the service, which returns the ARN in the X-Amz-Durable-Execution-Arn response header of Invoke -- the same header this code already builds for start-lambda and start-api callers, which is why the message is gated to the CLI path by the existing _is_cli_context() check.

Only the ARN is announced. Command suggestions are left where they already were, in the completion output, because at the point the execution starts they are not the next thing to run -- the invoke still owns the terminal, and the commands are for use from a second one. The completion summary is otherwise unchanged, so a short execution names its ARN twice: at the start, where it is needed in order to poll, and at the end alongside the result.

The banner goes to stderr, following the convention that stdout carries only the function result.

UX preview

All output below is from real runs against tests/integration/testdata/durable.

Before

Nothing identifies the execution until it is over. The ARN first appears in the completion summary, by which point there is nothing left to inspect:

$ sam local invoke HelloWorld --no-event
Invoking hello_world.hello_world.handler (python3.13)
Using local image: public.ecr.aws/lambda/python:3.13-rapid-x86_64.

Mounting .../.aws-sam/build/HelloWorld as /var/task:ro,delegated, inside runtime container
START RequestId: 8953e31b-4c14-4119-9e6e-61249315e34b Version: $LATEST
END RequestId: 8953e31b-4c14-4119-9e6e-61249315e34b
REPORT RequestId: 8953e31b-4c14-4119-9e6e-61249315e34b  Duration: 13713.41 ms  Billed Duration: 13714 ms  Memory Size: 128 MB  Max Memory Used: 128 MB

Execution Summary:
=========================
ARN:      9c8ef159-3f65-44d9-b24f-69bd0284a981/13ba6659-a849-47a9-9139-a11513f0d843
Name:     N/A
Duration: 13.68s
Status:   SUCCEEDED βœ…
Input:    {}
Result:   {"statusCode": 200, "body": "{\"message\": \"Hello, World!\"}"}

Commands you can use next
=========================
[*] Get execution details: sam local execution get 9c8ef159-3f65-44d9-b24f-69bd0284a981/13ba6659-a849-47a9-9139-a11513f0d843
[*] View execution history: sam local execution history 9c8ef159-3f65-44d9-b24f-69bd0284a981/13ba6659-a849-47a9-9139-a11513f0d843

After

The ARN is announced before START, i.e. before the handler has begun running. Everything else is unchanged:

$ sam local invoke HelloWorld --no-event
Invoking hello_world.hello_world.handler (python3.13)
Using local image: public.ecr.aws/lambda/python:3.13-rapid-x86_64.

Mounting .../.aws-sam/build/HelloWorld as /var/task:ro,delegated, inside runtime container

Durable execution started
=========================
ARN: 26af376b-5300-40ae-8e79-fe1032020f4f/77e70d5f-78b8-4c3f-a00f-6e2fcc8143f7

START RequestId: 0cb95694-e145-4a9a-aa40-2d4662ea68b1 Version: $LATEST
END RequestId: 0cb95694-e145-4a9a-aa40-2d4662ea68b1
REPORT RequestId: 0cb95694-e145-4a9a-aa40-2d4662ea68b1  Duration: 8101.15 ms  Billed Duration: 8102 ms  Memory Size: 128 MB  Max Memory Used: 128 MB

Execution Summary:
=========================
ARN:      26af376b-5300-40ae-8e79-fe1032020f4f/77e70d5f-78b8-4c3f-a00f-6e2fcc8143f7
Name:     N/A
Duration: 8.19s
Status:   SUCCEEDED βœ…
Input:    {}
Result:   {"statusCode": 200, "body": "{\"message\": \"Hello, World!\"}"}

Commands you can use next
=========================
[*] Get execution details: sam local execution get 26af376b-5300-40ae-8e79-fe1032020f4f/77e70d5f-78b8-4c3f-a00f-6e2fcc8143f7
[*] View execution history: sam local execution history 26af376b-5300-40ae-8e79-fe1032020f4f/77e70d5f-78b8-4c3f-a00f-6e2fcc8143f7

What the ARN unlocks

This is the case the issue is about. Terminal 1 invokes WaitForCallback, which suspends and does not reach a terminal state on its own:

$ sam local invoke WaitForCallback --no-event

Durable execution started
=========================
ARN: b47c8cee-7154-4585-abb4-a26e6ce6af1c/e7a4cbc7-ac4d-48be-b210-4c8500804702

START RequestId: 91ef2f77-df00-4aa7-8238-d5a48f87312c Version: $LATEST

πŸ”„ Execution is waiting for callback: eyJhcm4iOiJiNDdjOGNlZS03MTU0LTQ1ODUtYWJiNC1hMjZlNmNlNmFmMWMvZTdhNGNiYzctYWM0ZC00OGJlLWIyMTAtNGM4NTAwODA0NzAyIiwib3AiOiI5NzRiZmM2MzIxZTIyN2IwZmYxMDEzNDJlYWYxOWIzMzZmMDAyZGM2Y2M5OWMxOGYwOTQxYTc3NTVjYzIwZmM0In0=
Choose an action:
  1. Send callback success
  2. Send callback failure
  3. Send callback heartbeat
  4. Stop execution
Enter choice [1]:

Terminal 2, using the ARN from the banner while terminal 1 is still suspended:

$ sam local execution get b47c8cee-7154-4585-abb4-a26e6ce6af1c/e7a4cbc7-ac4d-48be-b210-4c8500804702

Execution Summary:
=========================
ARN:      b47c8cee-7154-4585-abb4-a26e6ce6af1c/e7a4cbc7-ac4d-48be-b210-4c8500804702
Name:     N/A
Duration: N/A
Status:   RUNNING
Input:    {}

$ sam local execution stop b47c8cee-7154-4585-abb4-a26e6ce6af1c/e7a4cbc7-ac4d-48be-b210-4c8500804702
πŸ›‘ Execution stopped: b47c8cee-7154-4585-abb4-a26e6ce6af1c/e7a4cbc7-ac4d-48be-b210-4c8500804702

Terminal 1 then finishes on its own:

Execution Summary:
=========================
ARN:      b47c8cee-7154-4585-abb4-a26e6ce6af1c/e7a4cbc7-ac4d-48be-b210-4c8500804702
Name:     N/A
Duration: 28.64s
Status:   STOPPED ⚠️
Input:    {}
Error:    Unknown: Execution stopped by user request

On develop neither of the two commands above can be issued at all, because the ARN is not known until the execution has already ended.

Stream split

The banner is diagnostic output, so it goes to stderr and stdout still carries only the summary and result:

$ sam local invoke HelloWorld --no-event 2>/dev/null

Execution Summary:
=========================
ARN:      26af376b-5300-40ae-8e79-fe1032020f4f/77e70d5f-78b8-4c3f-a00f-6e2fcc8143f7
Name:     N/A
Duration: 8.19s
Status:   SUCCEEDED βœ…
Input:    {}
Result:   {"statusCode": 200, "body": "{\"message\": \"Hello, World!\"}"}

Commands you can use next
=========================
[*] Get execution details: sam local execution get 26af376b-5300-40ae-8e79-fe1032020f4f/77e70d5f-78b8-4c3f-a00f-6e2fcc8143f7
[*] View execution history: sam local execution history 26af376b-5300-40ae-8e79-fe1032020f4f/77e70d5f-78b8-4c3f-a00f-6e2fcc8143f7

Which issue(s) does this change fix?

Part of #9200

Why is this change necessary?

The execution ARN is the only handle the sam local execution commands accept, and it was not available until the execution had already reached a terminal state -- so a running, suspended or interrupted execution could not be inspected.

How does it address the issue?

It prints the ARN as soon as the emulator returns it, before the invoke starts waiting.

What side effects does this change have?

sam local invoke on a durable function writes one additional three-line banner to stderr. stdout is unchanged.

Mandatory Checklist

PRs will only be reviewed after checklist is complete

Testing: unit tests pass across tests/unit/lib/utils/test_durable_formatters.py and tests/unit/local/docker/test_durable_lambda_container.py, five of them new -- the formatter, _show_execution_started in CLI context, in HTTP context and with no ARN, and an ordering test that asserts through a get_durable_execution side effect that the ARN was already printed by the time the execution was first polled. assert_invoke_output in the integration base used to find the summary's ARN by taking the first ARN: match in stdout; the lookup is now anchored to the summary section and the banner is asserted separately against stderr. The durable invoke and execution integration tests were run locally to produce the transcripts above. black, ruff and mypy clean.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@github-actions github-actions Bot added area/local/start-api sam local start-api command area/local/invoke sam local invoke command area/local/start-invoke pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at. labels Aug 27, 2026
sam local invoke printed the durable execution ARN only after the execution reached a
terminal state, and otherwise logged it at DEBUG. That ARN is the only identifier sam
local execution get, history and stop accept, so an execution that was still running --
suspended on a callback, waiting out a context.wait, or interrupted with Ctrl-C -- could
not be inspected without re-running the invoke under --debug and reading it out of the
log.

--durable-execution-name is not a way around it. The emulator builds the ARN from a pair
of generated UUIDs and keeps the name only as metadata, so the execution commands answer
"Execution <name> not found" when given a name. Nothing else about a running execution is
missing: GetDurableExecution and GetDurableExecutionHistory both return data mid-flight,
so the ARN was the only gap.

The ARN is now printed as soon as start_durable_execution returns, before the wait
begins, together with the get/history/stop commands that work from another terminal while
the execution runs. That mirrors the service, which returns the ARN in the
X-Amz-Durable-Execution-Arn response header of Invoke -- the same header this code already
builds for start-lambda and start-api callers, which is why the message is gated to the
CLI path by the existing _is_cli_context() check. The completion summary is unchanged, so
a short execution now names its ARN twice: at the start, where it is needed in order to
poll, and at the end alongside the result.

The suggested commands are left unquoted, matching the existing completion output. Once
the emulator emits service-shaped ARNs containing $LATEST, both call sites will need
quoting, and that belongs with the ARN format change rather than here.

assert_invoke_output in the integration base used to find the summary's ARN by taking the
first "ARN:" match in stdout; the banner now precedes it. The value is the same either way,
so nothing failed, but the helper had stopped proving that the summary carries the ARN. The
lookup is now anchored to the summary section, and the banner is asserted separately, so
both halves of the user-visible output are covered.

This covers the first of the three options aws#9200 offers. Listing local executions and
accepting --durable-execution-name in the execution commands are not addressed here.

Testing: 86 unit tests pass across tests/unit/lib/utils/test_durable_formatters.py and
tests/unit/local/docker/test_durable_lambda_container.py, five of them new -- the
formatter, _show_execution_started in CLI context, in HTTP context and with no ARN, and an
ordering test that asserts through a get_durable_execution side effect that the ARN was
already printed by the time the execution was first polled. The existing sync
wait_for_result test now expects both secho calls, in order. The durable integration tests
need Docker and were not run; the reworked assertions in assert_invoke_output were checked
against a reconstruction of a full invoke stdout, confirming the anchored lookup returns
the summary ARN and that the banner assertions hold. black, ruff and mypy clean.
@ParidelPooya
ParidelPooya force-pushed the fix/show-durable-execution-arn-at-start branch from d194dd6 to 2babdb7 Compare August 27, 2026 19:00
@ParidelPooya
ParidelPooya marked this pull request as ready for review August 27, 2026 20:44
@ParidelPooya
ParidelPooya requested a review from a team as a code owner August 27, 2026 20:44

@roger-zhangg roger-zhangg 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.

Thanks for the quick fix, 2 small comment

Comment thread samcli/lib/utils/durable_formatters.py Resolved
Address review feedback on the durable execution started banner.

The command list is now rendered by the existing
generate_next_command_recommendation helper instead of being formatted
inline, so the banner matches the next-command sections the rest of the
CLI prints. The custom lead-in line becomes a standalone sentence above
the shared "Commands you can use next" heading.

The banner is diagnostic output, so it goes to stderr, following the
convention that stdout carries only the function result. The completion
summary is unchanged and stays on stdout.

assert_invoke_output now takes an optional stderr argument and looks for
the banner there; the three call sites that capture the two streams
separately pass it. The streaming callback test merges stderr into
stdout, so it keeps using the lookup anchored to the summary section.

@bchampp bchampp 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.

This is a regression. We shouldn't be displaying the "next" commands that can run before the invoke happens.

The started banner listed the get/history/stop commands under a
"Commands you can use next" heading. At the point the execution starts
those commands are not next: the invoke still owns the terminal, and the
commands are for use from a second one, so the heading described the
wrong thing.

Show just the ARN. Command suggestions stay in the completion output,
where "next" is accurate, which also means the started banner no longer
duplicates the command list and the shared
generate_next_command_recommendation helper is left untouched.

The _execution_commands helper added earlier in this branch existed only
to share the tuples between the two sections and now has a single
caller, so format_next_commands_after_invoke goes back to building them
inline. The net change to durable_formatters.py is one added function.

@aws-sam-tooling-bot aws-sam-tooling-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review Results

Reviewed: 7ca0a8b..18903f5
Files: 7
Comments: 1

The docstring still claimed the ARN is displayed together with the
commands that can be used to inspect a running execution. Only the ARN
is printed; the command suggestions stay in the completion output.
@ParidelPooya

Copy link
Copy Markdown
Contributor Author

Updated for all the feedback, and the PR description now has a UX preview section with real transcripts (sam local invoke before/after, plus the mid-flight case).

@roger-zhangg the output now matches the shape you sketched:

$ sam local invoke HelloWorld --no-event
Invoking hello_world.hello_world.handler (python3.13)
Using local image: public.ecr.aws/lambda/python:3.13-rapid-x86_64.

Durable execution started
=========================
ARN: 26af376b-5300-40ae-8e79-fe1032020f4f/77e70d5f-78b8-4c3f-a00f-6e2fcc8143f7

START RequestId: 0cb95694-e145-4a9a-aa40-2d4662ea68b1 Version: $LATEST
END RequestId: 0cb95694-e145-4a9a-aa40-2d4662ea68b1
REPORT RequestId: 0cb95694-e145-4a9a-aa40-2d4662ea68b1  Duration: 8101.15 ms  Billed Duration: 8102 ms

Execution Summary:
=========================
ARN:      26af376b-5300-40ae-8e79-fe1032020f4f/77e70d5f-78b8-4c3f-a00f-6e2fcc8143f7
Status:   SUCCEEDED βœ…
...

Commands you can use next
=========================
[*] Get execution details: sam local execution get 26af376b-...
[*] View execution history: sam local execution history 26af376b-...

One thing I left out deliberately: your sketch had ------ separators around the invocation logs. Adding those means changing how the runtime container emits logs, which would affect every sam local invoke, not just durable ones β€” bigger than this PR and worth doing on its own if you want it. The ===== rules under the two section headings already give the separation, as above. Happy to add literal separators here instead if you would rather.

The most useful part of the preview is the case the issue is actually about: invoke WaitForCallback, which suspends and never terminates on its own, then use the ARN from the banner in a second terminal while the first is still hung β€” execution get returns Status: RUNNING, execution history returns the events mid-flight, and execution stop unblocks it. On develop none of those can be issued at all, because the ARN is not known until the execution has already ended.

@bchampp @zhongkechen the new commits reset your approvals, sorry about the churn β€” would appreciate another look.

auto-merge was automatically disabled September 2, 2026 22:03

Merge commits are not allowed on this repository

@roger-zhangg
roger-zhangg merged commit c761540 into aws:develop Sep 2, 2026
53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/local/invoke sam local invoke command area/local/start-api sam local start-api command area/local/start-invoke pr/external stage/needs-triage Automatically applied to new issues and PRs, indicating they haven't been looked at.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants