Repository navigation
Phase 6b-1: post workflow results back into chat (server and status route) - #1610
Merged
Paul Lizer (paullizer) merged 39 commits intoOct 2, 2026
Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Seeds the run's chat_delivery record behind Use Workflow Results In Chat, flags the sidecar, and promises the result in the chat only when every started run will be posted back. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…me projection Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… a guarded unread mark Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Claims a run's chat_delivery record with an ETag-fenced lease, re-checks the chat, the run, the runtime control and both gates, defers mid-stream, composes a bounded fenced reply or a fixed note, marks the chat unread before the deterministic message create, and sends one idempotent notice. Closed chats get no message and one notice. Crashes, outages, deferrals, deletions and durable resumes never post twice or retry forever. Bumps VERSION to 0.261.218. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Runs the hint-driven delivery worker in a background thread with a throttled, lock-guarded sweep. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Classic Retry and Edit return a closed 400 code for delivered results and notes before any content check or write; the V2 orchestration routes already answer 404 for a delivery message ID. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…re every id for guarded unread marks (6b-1) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…e-guard tests (6b-1) A record reverted by a stale save (phase none) skipped the point read of the deterministic message and re-ran compose, the unread mark and the notice call. The worker now point-reads the message whenever the phase is below message_created, and exhaustion checks it at any phase. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…release notes (6b-1) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
app.py binds sessions to the first offline app's directory, so the later offline apps in one process recreate it after the first teardown removes it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Pins the pure contract module: message and thread identity per generation, idempotency key formats, labels and notes, the metadata snapshot, backoff, seeds, and the reconcile reopen, tombstone, missing-control and expiry rules. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Barrier-forced claim race, hint versus sweep race, a stale worker whose lease was reclaimed, and repeated sweeps; each delivers exactly once. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Fresh-process probes import the new delivery modules in both orders, with network calls blocked, and check the background loop and status route wiring. The probes bootstrap config and functions_settings first, as app.py does, because of a pre-existing import cycle through functions_document_actions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The fake sweep now matches a branch only when its clause appears in the production query, so removing the expiry branch fails a test. New worker tests cover the expiry notice past the window and a lapsed-lease takeover. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The notice tests import config with a fake Cosmos client. A module fixture now forgets app modules imported in that file, so a later file's offline app binds its own containers when the suites run in one process. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Assertion messages in the delivery suites now name the broken invariant, refusals from the strict status-query fake surface as FakeCheckFailed (and the status-route suite imports it), and unused imports are removed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
One combined -O pytest run hit the 300s cap on a probe that takes about 8s. The child now arms a faulthandler watchdog at 240s, and run_probe names the probe and says whether it stalled or exited non-zero. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The claim-race check now says one worker delivers and the other stays busy and prints sorted results, so the message is the same in both modes. deliver_until says the delivery must settle within the attempt limit and counts what it kept returning. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The status-route body test now checks the exact row, delivery and action key sets, that workflow_scope is personal, and that the documented response example matches. The feature doc now says what happens at the attempt cap before and after a claim. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- notice_and_unread: patch azure.cosmos.CosmosClient with unittest.mock.patch during the guarded import instead of storing the module attribute, which cleared the two py/import-of-mutable-attribute alerts it surfaced. - concurrency: _run_threads catches Exception instead of BaseException (py/catch-base-exception) and fails readably when a delivery thread ends without a result, so a worker stopped by a non-Exception BaseException still fails the test. - off_golden: add the functional-test sys.path block before the sibling imports so the file runs without PYTHONPATH. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Kill review survivors Exhausted, UnreadGone, X10, PlaceAfter and BadIds: an abandoned claim at the attempt cap closes undeliverable without a read or compose (and delivered without reposting at message_created); a chat lost during the guarded unread mark closes undeliverable; a workflow deleted during the result read closes silently while a missing result for a live workflow notifies once; invalid ids return before any read; and a delivery is stamped after a newer or same-instant assistant message. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Kill review survivors LogSwallow, DueNow, MergePop and P3: delivery_log swallows a log_event failure; a deferred pending record that turns terminal comes back ready and due now; merging never recreates a chat_delivery the store lacks; and the guarded run replace sends the ETag from the read just before it, so only a real concurrent write raises 412 and the concurrent delivery wins. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…lows Kill review survivors S11, S12 and S15: a status row exposes the delivery message id only for a delivered record whose id carries the delivery prefix; a failed run with an unknown gate reason reports error_code failed with the generic sentence; and a workflow document owned by another user or a group blocks retry as workflow_deleted. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…f case Kill review survivors StatusFilter, RunApplies, M17b and R2b: a start whose queued result is already completed, or whose run lacks an applicable record (none, another chat, wrong version), is not flagged for delivery and keeps the plain follow-up; two started runs where only one delivers use the plain many follow-up; and the runtime projection never saves a chat_delivery the stored run lacks, nor queues a hint. Fix the vacuous gate-off case: the queue fake read options["chat_delivery"] and raised KeyError when the gate was off, so _start returned unavailable. It now reads the seed with .get() and the test requires the off case to reach a queued start. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Kill review survivors U10 and U6: a chat read at exactly the skip instant counts as read, so the guarded mark returns read_since without writing; and with require_private=False the mark still refuses another user's chat as unavailable without writing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Step 9 (microsoft#1607, 0.261.217) merged first, so this branch merges V2 second with rerere disabled, as agreed: - config.py keeps VERSION 0.261.218. - release_notes.md keeps the 0.261.218 section on top, followed by the whole 0.261.217 section. - docs/_data/app_surface.yml was regenerated after the merge with scripts/build_docs_inventory.py and has no diff. - route_backend_orchestration.py, docs/admin/workflow.md, docs/admin/orchestration.md and CHAT_WORKFLOW_RESULTS_FOLLOW_UP.md merged cleanly; all 6b-1 doc lines remain. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer)
marked this pull request as ready for review
October 2, 2026 21:28
) into 6b-1 Merged with rerere disabled. Two files conflicted: - application/single_app/config.py: kept this branch's VERSION line for the merge; the next commit renumbers 0.261.218 to 0.261.226 because V2 is now at 0.261.225. - docs/explanation/release_notes.md: this branch's section stays on top, followed by V2's 0.261.224 to 0.261.218 sections unchanged. route_backend_conversations.py merged cleanly (hunks do not overlap). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 is now at 0.261.225 (microsoft#1612 with microsoft#1608), so this branch takes the next version. Mechanical change only: 51 occurrences in 21 files, all on lines this branch added (config.py VERSION, the release-notes heading, test headers, route-inventory docstrings and the feature docs). V2's own 0.261.218 lines from microsoft#1608 are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Merged with rerere disabled. microsoft#1620 also moved V2 to 0.261.226, the number this branch had just taken, so config.py merged without a conflict (both sides made the same VERSION change); the next commit renumbers this branch to 0.261.227. docs/explanation/release_notes.md conflicted because both sides added a 0.261.226 section at the top. This branch's section stays on top, then microsoft#1620's section under its own heading, unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
microsoft#1620 moved V2 to 0.261.226 while this branch held it, so this branch takes the next version. Mechanical change only: the same 51 lines in 21 files that the previous renumber touched. V2's own 0.261.226 lines from microsoft#1620 are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer)
merged commit Oct 2, 2026
ac63b3a
into
microsoft:paullizer-react-v2-ui
10 checks passed
This was referenced Oct 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
workflow_run), the plan still ends right away, but the server now posts the run's terminal outcome back into that private chat, even hours later. Each delivery generation gets exactly one post under a dated label (Results from `X` · you asked on …), one unread mark and one bell notification. The post is either a reply composed from a bounded, nonce-fenced excerpt (6a's reader and fence) or a fixed application note: failed, cancelled, skipped, saved analysis, content blocked, or status.GET /api/v2/orchestration/workflow-runs/status[?conversation_id=…], and a fixed message-metadata contract (metadata.workflow_delivery). Classic Retry/Edit refuse any message a workflow run posted, with a fixed 400{error, code}.enable_chat_workflow_results, default Off), checked role-aware at start and again at delivery, plus the existing Run Workflows From Chat gates. With the setting off, behavior is byte-identical to the base; the off-golden test pins this.Linked issue
Refs #1546
Refs #1543
Part of #1546 (Phase 6, step 6b-1, the server half) and the #1543 umbrella. 6b-2 (
WorkflowRunCard, app-shell tracker,WorkflowRunningTag, V2 run deep link) stacks on this branch and builds against the status-route contract below.Release Notes & Latest Features
Is this visible to end users?
Is this admin-facing (Admin Settings, governance, deployment, config)?
Should this become a Latest Feature card?
Screenshot needed for the card?
Version bump
application/single_app/config.pyVERSIONthird segment bumped, or not needed because this is docs-only (0.261.226→0.261.227; first.218, renumbered after V2 merges, see Step 9 overlap)deployers/version.txtbumped, or not needed becausedeployers/was not changed (not changed)Testing / validation
Environment: Python 3.12 venv, Windows. The related-suite comparison used
PYTHONPATH=application\single_app;functional_tests; the delivery suites now run without it. Base for the related-suite comparison:b5cc18ee2, then thepaullizer-react-v2-uitip and this branch's merge-base, in a clean detached worktree.Before opening I merged
origin/paullizer-react-v2-uiwithgit -c rerere.enabled=false merge; it was already up to date atb5cc18ee2. Step 9 (#1607), then #1612 (with #1608) and #1620 merged into V2 first, soedd748417,35ce8ed58and389c9b14bmerge each new tip the same way (Step 9 overlap).VERSIONis0.261.227. My last production change is74f420870; my later commits touch only tests and docs, apart from the V2 merges and two renumber commits. The related-suite comparison ran at766fea19d. Atedd748417, after the review-survivor tests (Mutations → Review survivors), and again at the head543f1a5a8, I reran the delivery suites four ways (table below) and the validation set below, which gave the same counts, plain and-O. At543f1a5a8, the 22 test files V2 changed since8a802f196(#1608, #1612, #1620), including #1608'stest_workflow_background_identity.py, pass plain and-O, excepttest_simplechat_conversation_messages.py(3 failures, the same on a clean1524eeb7b: no Cosmos credential). Three UI files andtest_v2_orchestration_workflow_results_input.pypass once the gitignoredui_tests/artifacts/orchestration-plan-editorbuild exists. After each V2 merge the UI suites need a localnpm --prefix application/v2_ui run build(the bundle is gitignored);tsc -bpassed. Step 9's suites and related ones (44 files) also ran onedd748417and on a clean8a802f196, plain and-O(Stalls under load).Delivery suites: 13 files, 383 tests, four ways
python -m pytest(13 files together)python -O -m pytest(13 files together)python <file>, once per filepython -O <file>, once per filefunctional_tests\.workflow-result-chat-state-*directoriesDocs
Related suites against the base, normal and
-OEach of 221 files ran on HEAD and on a clean, detached worktree of the base
b5cc18ee2, usingpython [-O] -m pytest <file> -q -rfE -p no:warnings -p no:cacheprovider, 6 files at a time with a 1,200 s limit per file. The 221 files:test_v2_workflow_loops.py,test_v2_group_participant_invites.py,test_v2_notifications_bell.pyandtest_v2_orchestration_run_hydration.py-Onode_modulesand no built V2 SPAtest_log_injection_codeql_alerts_fix.pyon HEADtest_v2_chat_phase1_fixes.pyandtest_workflow_calendar_schedules.pyon HEAD;test_public_document_publication.pyon the baseApart from the load stalls, no test fails on HEAD that passes on the base. The 67 files with identical failures are listed at the end of this section.
Route policy (normal and
-O, same counts):route_tests/test_route_blueprint_policy_inventory.py: 11 passed (base 10). The new test pins the status route's decorator policy.route_tests/test_route_unauthenticated_policy_contract.py: 6 passed (base 5). The new test requires a signed-in session for the status route.route_tests/test_route_policy_test_coverage.py: 2 passed (base 2).#1598 suites (normal and
-O, same counts):b5cc18ee2ui_tests/test_v2_workflow_loops.pyfunctional_tests/test_group_collaboration_source_storage_fix.pynon_group_inviteesubtests: storageregularandgroupintest_rejections_do_not_read_history_or_mutate_storesandtest_v1_route_preserves_rejection_statuses_without_mutationui_tests/test_v2_group_participant_invites.pyfunctional_tests/test_v2_prompt_attachment_persistence.pyNo new seed was needed. Of these,
test_v2_workflow_loops.pyexercises the changed_project_runtime_runand run save.Other V2 suites (normal and
-O):ui_tests/test_v2_notifications_bell.py: 43 passed. The base worktree errors without the built SPA.functional_tests/test_v2_notifications_bell.py: 10 passed. On the base, 1 fails because itsnpm --prefix application/v2_uistep needsnode_modules.ui_tests/test_v2_orchestration_run_hydration.py: 6 failed, 1 passed on both trees (pre-existing).Stalls under load (environmental: CPython WMI hang)
A few fresh-process probes stalled, only during the comparison: 6 pytest processes across two trees, alongside another session's mutation runner. The imports test's 240 s
faulthandlerwatchdog caught the stack:functions_authenticationL8 →configL39 →functions_appinsightsL12 →azure/monitor/opentelemetry/exporter/_utils.pyL172 →platform.node()→_wmi.exec_query, which blocks (python/cpython#125315). The chain is byte-identical on the base, and this PR doesn't touch it. In a stall hunt, 155 of 156 cold-import probes finished, each within 21.1 s. Solo reruns were clean:test_v2_chat_phase1_fixes.py -O(5/5 runs, 10 passed each),test_workflow_calendar_schedules.py -O(5/5 runs, 512 passed each) andtest_log_injection_codeql_alerts_fix.py(44 passed, normal and-O; its one failure under load was a cold-import probe through the same chain).The 67 files that fail identically on HEAD and the base (pytest tails; each is the same normally and with `-O`)
Functional-test names drop
test_and.py. Tails: F failed, P passed, E errors, S subtests passed.admin_release_notifications_registrationadvanced_conversation_search_matching_fixagent_stream_tool_retry_fallbackai_connection_text_consumersanalyze_workflow_publication_integrationapproval_notification_routing_fixbackend_conversations_swagger_integrationbackground_task_distributed_locksbroken_access_control_findings_fixbroken_access_control_guardrails_checkerchat_completion_notificationschat_early_conversation_titlechat_retry_thought_tracker_init_fixchat_stream_background_executionchat_stream_compatibility_sse_syntaxchat_stream_debug_loggingchat_stream_heartbeat_reattachchat_stream_history_context_fixchat_stream_lifecycle_observabilitychat_stream_new_conversation_reattachchat_stream_retry_context_init_fixchat_stream_stop_controlchat_streaming_sse_metadata_fixchat_upload_personal_workspace_handoffcollaboration_conversation_summary_export_fixcollaboration_message_notificationscontent_screening_pipelinecontent_screening_reviewscontrol_center_auto_refresh_scheduleconversation_forkconversations_read_ownership_authorizationcosmos_throughput_background_schedulerdeployer_background_tasks_app_settingdocument_actions_and_comparison_featuredocument_analysis_featuredocument_analysis_progress_and_limitsgeneric_workflow_automationgroup_collaboration_source_storage_fixgroup_file_share_approval_notificationsgroup_notification_contextgroup_workflows_featuregunicorn_startup_supportmessage_ordering_with_retrymixed_source_chat_search_consistencynotification_polling_redirect_guardpersonal_conversation_followup_authorizationpersonal_workflows_featurescope_lock_collaboration_unlock_fixshared_conversation_file_approval_fixsimplechat_operation_notificationsstartup_scheduler_supporttabular_retry_sheet_recoverytabular_tool_error_retry_and_thoughtsthoughts_featurev2_chat_context_pickerv2_collaboration_visual_style_fixv2_conversation_deep_linkworkflow_access_controlsworkflow_cancellationworkflow_file_sync_triggersworkflow_m365_rebase_integrationworkflow_priority_alertsworkflow_runtime_integrationworkflow_stepped_builderworkflow_task_document_actionsworkflow_task_result_handoffui_tests/test_v2_orchestration_run_hydration.pyDocumentation
### **(v0.261.227)**, New Features: Workflow results posted back to chat)docs/explanation/features/CHAT_WORKFLOW_RESULT_DELIVERY.md, the full 6b-2 contract; cross-links inserted inCHAT_ORCHESTRATION_WORKFLOW_RUNS.md,CHAT_WORKFLOW_RESULTS_FOLLOW_UP.md,docs/admin/orchestration.md,docs/admin/workflow.md,docs/guides/trigger-a-workflow.md)Security checklist
@swagger_route(security=get_auth_security())(the status route:@swagger_route→@login_required→@user_required→@enabled_required('allow_user_workflows')→@workflow_user_required; route-policy inventory and the unauthenticated contract are updated)sanitize_settings_for_user()(no settings are sent; the status route returns a projected row shape and a booleanavailable)Decisions
The full rationale is in the feature doc. In short:
chat_deliverygoes through a guarded save (save_personal_workflow_run): an ETag replace that re-reads and keeps the storedchat_deliveryandchat_invocation, so the runner's long-held run dicts can't wipe them. Runs withoutchat_deliverykeep the plain upsert. A check-before-compose at every phase beforemessage_createdmeans a lost reply is found, not re-posted.versionat the terminal projection.runtime/resume(store.resume/journal_request('resume')) bumps the version and keeps therun_id. A record that isdelivered/undeliverablereopens only whencontrol.version > generationandnot control.deleted(A1).tombstone()is the only other write that bumps a terminal control's version, and it setsdeleted. A fresh run never reuses a generation because it has a newrun_id, and the message id isassistant_workflow_delivery_{fingerprint(run_id, generation)}. Expired records never reopen._chat_delivery_seedchecks the cheap setting, thenis_chat_workflow_results_enabled_for_user). The requester's roles are stored inchat_delivery.requester_roles. At delivery,is_user_workflows_enabled_for_userandis_chat_workflow_results_enabled_for_userare re-evaluated against those stored roles. If workflows are off for the requester, nothing is posted and oneaccess_lostnotice is sent. If results are off, the post is a fixed status note with no result text. Role revocations after start aren't seen (see Residual risks).workflow_resultdescriptor andworkflow_result_contexts, so every read re-authorizes it (masking), Follow up works, and later turns inherit masking.metadata.workflow_deliveryor theassistant_workflow_delivery_id prefix: 400{error, code}withworkflow_delivery_retry_unsupported/workflow_delivery_edit_unsupported.expires_at: the control'sdeadline_atplus a 24-hour grace (DELIVERY_GRACE_SECONDS). A v1 control has nodeadline_at, socompute_expires_atuses the record's creation (or reopen) time plus the run's capped deadline, or the default, plus the same grace. A run that is still non-terminal past it, or paused ondeadline_exceeded, closesexpiredwith one fixed expired notice and no post. A run that is terminal but still unpublished past it posts nothing and closesundeliverablewith reasonexpired_before_deliveryand one undeliverable notice.RECENT_USER_MESSAGE_SECONDS). Fifteen minutes after the first deferral (DEFER_MAX_SECONDS) those signals are ignored and it posts. A failed newest-message read defers rather than counting as empty.skip_if_read_since) → create message (409 = already created) → notice (deterministic key) → recorddelivered.check_chat_contenton the composed answer, and a trimmed fallback that is also safety-checked. Token usage is logged like 6a's_log_usage, plusadditional_context={'workflow_result_delivery': True}andidempotency_key=f'workflow_result_delivery:{M}:{attempt}'(A4).assistant_workflow_delivery_{fingerprint(run_id, generation)}). At post time the message gets its own thread (delivery_thread_id(M)), withprevious_thread_idset to the newest message's thread, and a timestamp of now, or 1 µs after the newest message if that is later. It lands at the end of the chat, not under the turn that started the run.chat_deliveryrecord holds codes, IDs, times, roles and model IDs, never result text.chat_delivery: True, set only for a queued or running run wherechat_delivery_appliesholds). It then usesWORKFLOW_RUN_DELIVERY_FOLLOW_UP("I'll post the results here when the run finishes. You can also follow it in the workflow's run history in Workflows.") or its plural form. If any started run lacks delivery, the note keeps the Phase 5 text unchanged.Terminal writes
Every write that can make a chat-started run terminal, expired or reopened is covered by the trigger (projection reconcile plus signal), the sweep, or both:
failed)completed,completed_partial)readywithkind: result, signal and sweep.failed)chat_deliveryandchat_invocation, including the double-fault path._project_runtime_runpaused+deadline_exceeded; the sweep also catchesexpires_at.runtime/resumeversion > generationand the control isn't deleted.runtime/decisionapprove/rejectresume-failedchat_invocation.workflow_deleted. A deleted run has nothing to deliver.chat_delivery. Group runs belong to Phase 7 (#1550).Leak surfaces
workflow_result+workflow_result_contexts, so every read re-authorizes it and withholds it when the run was deleted, the result changed, source access was lost, storage failed, or the chat isn't private (6a masking).test_masking_surfaces_withhold_delivered_result_and_matching_followup_after_run_deleted.Results from "X" are in your chat); metadata is IDs and codes only.type(exc).__name__only; user-controlled strings go throughsanitize_log_message; never content, neverstr(exc)(M13).errortext from a map;step_labelis alwaysnull.chat_deliveryrecordlast_updated.Status-route contract (fixed for 6b-2)
GET /api/v2/orchestration/workflow-runs/status[?conversation_id=<id>]. It's placed after the per-answer links route inroute_backend_orchestration.py.@bp.route(..., methods=["GET"]),@swagger_route(security=get_auth_security()),@login_required,@user_required,@enabled_required('allow_user_workflows'),@workflow_user_required.conversation_id: optional, at most once, trimmed,[A-Za-z0-9_-]{1,128}. Empty, repeated, too long or malformed → 400{"error": "The conversation ID is not valid.", "code": "invalid_conversation_id"}. An invalid server-side user id or a storage/settings failure → 503{"error": "Workflow run status isn't available right now. Try again later.", "code": "workflow_run_status_unavailable"}.partition_key=<session user_id>,c.user_id = @user_id,c.trigger_source = 'chat_orchestration',IS_DEFINED(c.chat_invocation), and a projected field list (noSELECT *).conversation_id,TOP 21, returning up to 20 of that chat's started runs. Without it,TOP 51, returning up to 50 non-terminal runs, open delivery records, or deliveries from the last 10 minutes. Both useORDER BY c.chat_invocation.requested_at DESC.truncatedis true when the query returned more than the limit. Everychat_invocationwriter setsrequested_at(test_workflow_run_chat_invocation_writer_sets_requested_at); the runs container uses the default indexing policy.live: false.availableis Phase 5'sworkflow_run_settings_gateat request time. Rows are still returned when it'sfalse, so an in-flight card doesn't vanish when an admin flips the setting mid-run.{ "available": true, "runs": [ { "workflow_id": "wf_123", "workflow_scope": "personal", "run_id": "run_123", "workflow_name": "Weekly digest", "conversation_id": "conv_123", "orchestration_run_id": "orch_123", "step_id": "step_2", "requested_at": "2026-01-05T02:30:00Z", "status": "running", "phase": "running", "runtime_version": 3, "step_index": 2, "step_count": 5, "step_label": null, "started_at": "2026-01-05T02:30:15Z", "completed_at": null, "elapsed_seconds": 416, "waiting": null, "delivery": { "status": "pending", "generation": 3, "message_id": null, "delivered_at": null, "reason": null }, "error": null, "error_code": null, "retry_blocked": null, "actions": { "cancel": true, "retry": false, "approve": false, "open_run": true }, "live": true } ], "checked_at": "2026-01-05T02:37:11Z", "truncated": false }Key field rules (the full tables are in the feature doc, Status route contract for 6b-2):
status/phase:statusphasequeuedqueuedrunningrunning,cancelling,waiting_recovery, unknown statesrunningrunningwaitingneeds_youcompletedcompletedfinishedcompleted_partialcompleted_partialfinishedfailed,invalid,incomplete,skippedfailedfailedcancelledcancelledcancelledexpired, or paused +deadline_exceededexpiredfailedwaiting:{reason, action, gate_id}wherereasonis one ofapproval,microsoft_365_reconnect,microsoft_365_approval,output_review,recovery,deadline_exceededorpaused, andactionisapprove,reconnectoropen_run. Awaiting_recoveryrow isrunning/runningbut still carrieswaiting: {reason: recovery, action: open_run}.delivery.status: one ofnot_applicable,pending(also covers storedready),delivering,delivered,undeliverableorexpired.message_idis set only whendeliveredand prefixedassistant_workflow_delivery_.reasonis set only when it is a closed delivery reason code.runtime_versionis theexpected_versionthatruntime/resumechecks (equality injournal_request). The live version is used when the live read succeeded, otherwise the run'sruntime_version, otherwise the stored projection version.test_schema_one_status_runtime_version_is_resume_expected_versionandtest_schema_two_…feed a row's value to resume on the real store over a fake container, and both succeed.actions.retryis true only when the state isfailed/invalid/incomplete(RESUMABLE_STATES) andretry_blockedis null.retry_blockedis checked in order:retry_unavailable(unknown eligibility, failed read, read cap exhausted, missing revision or revision error) →workflow_deleted(deletingor gone) →not_resumable(stored projectioncan_resumefalse) →deadline_exceeded(limits.deadline_at≤ now) →workflow_definition_changed(workflow_definition_revisionof the editor form ≠run.definition_revision, computed exactly likedecide_workflow_runtime) →workflow_already_running(active_run_idnot inNone,''or thisrun_id).skippedandcancelledare never retryable. There is one test per code (test_retry_blocked_codes,test_retry_blocked_workflow_read_errors_and_retry_ok,test_skipped_and_cancelled_are_never_retryable_even_with_can_resume).actions.cancel: non-terminal, not expired and notcancelling.actions.approve: waiting actionapprove, agate_idpresent, and not expired.actions.open_run: always true.Snapshot rule: a row is a snapshot. 6b-2's Retry must still handle a 409 from
runtime/resume(workflow_definition_changed,workflow_already_running,workflow_deleting,stale_version,invalid_state,deadline_exceeded,request_conflict), plus 400, 403, 404 and 503.Routes 6b-2 calls:
GET …/runs/<run_id>/runtime,POST …/runtime/decision,POST …/runtime/resumewith{expected_version, request_id}(a client UUID), andPOST …/cancel, all under/api/user/workflows/<workflow_id>. Notresume-failed: it returns 409 for durable runs.Message metadata for 6b-2
Every posted delivery message, including fixed notes, carries:
{ "workflow_delivery": { "version": 1, "kind": "result", "workflow_id": "wf_123", "workflow_scope": "personal", "run_id": "run_123", "generation": 3, "run_status": "completed", "orchestration_run_id": "orch_123", "step_id": "step_2", "requested_at": "2026-01-05T02:30:00Z" } }metadata.workflow_delivery. The server refusal also checks theassistant_workflow_delivery_id prefix. 6b-2 joins the live status row byrun_id,orchestration_run_idandstep_id(ordelivery.message_id).metadata.workflow_result, 6a's public descriptor snapshot, which is the samedict(result['descriptor'])6a's follow-up stores, plusmetadata.workflow_result_contexts. Only kindsresultandanalysiscarry them.workflow_id+run_id+workflow_scopefrommetadata.workflow_delivery.actions.retryis true. The metadata alone never makes Retry available.failed,cancelled,skipped,status(gate off at delivery, or the result can't be shown) andcontent_blockedcarry no 6a keys.analysiscarries the descriptor and contexts. Each kind's text is fixed (see the feature doc, Labels and application-owned texts).lib/workflowResults.tsparsesmetadata.workflow_resultstrictly (version,workflow_id/run_id, SHA-256, readable status). The worker stores 6a's own descriptor, so V2 accepts delivered results, hides Retry/Edit on them (messageAsksAboutWorkflowResult), and Follow up works through 6a.workflow_result, so today's V2 still shows Retry/Edit on them. The server refuses with 400{error, code}, and 6b-2 hides them usingmetadata.workflow_delivery.Tests
13 new files under
functional_tests/, 383 tests as pytest collects them. Each file also runs as a script through its own__main__.test_workflow_chat_delivery_contract.pyexpires_at; and the stored fields the save guard keeps, including cancellation state. Covers every reconcile branch: the amendment-1 reopen rule (version > generationand not deleted), silentworkflow_deletedandruntime_missingcloses, expired records never reopening, and capped history. Also the bounded hint queue. A terminal state makes a deferred record due at once; a merge drops an incomingchat_deliverythe stored run lacks;delivery_logswallows logging failures.test_workflow_chat_delivery_worker.pytest_workflow_chat_delivery_status_route.pyretry_blockedcode;skippedandcancellednever retryable. A row'sruntime_versionis accepted asexpected_versionby the real store's resume, in schema 1 and 2. Everychat_invocationwriter setsrequested_at. Fixed, non-leaking errors (workflow_run_status_unavailable). The route's sign-in and feature gates, and the exact top-level, row,deliveryandactionskeys, checked against the feature doc's response example. A message id needs a delivered row and the prefix; an unknown failure reason maps tofailed; a foreign or group workflow document blocks retry.test_workflow_chat_delivery_control_pins.pyWorkflowRuntimeStoreover a fake container, schema 1 and 2, for every terminal state. Every public store method is classified. Every non-resume mutator leaves a terminal control'sversionalone.tombstonebumps it and setsdeleted, after which the mutators stay pinned.resumeis the only non-delete bumper, and delivery reconcile reopens only for a real resume generation.test_workflow_chat_delivery_imports.py-O. The contract, worker, status module, routes and runtime load in every order after the app bootstrap and aroundbackground_tasks. The cold contract and status imports load neitherconfignor the runtime. Each child dumps its stacks and exits if it stalls for 240 s.test_workflow_chat_delivery_notice_and_unread.pyworkflow_chat_deliverynotice type matches the contract and the registry. Notices pass retry options, return the existing notice on a repeat key, and fail strictly on create errors. The guarded unread mark writes only against the ETag it read, keeps concurrent edits, and skips once the user has read since the planned time. It never writes for a missing or unavailable chat, and rejects missing ids. The read-since skip holds at the equal boundary, andrequire_private=Falsestill rejects another user's chat.test_workflow_chat_delivery_projection_and_seed.pychat_deliveryand the step sidecar only when the seed applies. The runtime projection keeps the stored record, reconciles it and signals the worker once. A reconcile or hint failure never blocks the runtime write. Covers resume reopening a delivered generation, a deleted control closing silently, deadline expiry, and the started-run follow-up text. Only a queued or running run with this chat's record is flagged, and a mixed pair of started runs gets the plain follow-up. The projection never copies the runner'schat_delivery. The gate-off case reaches a queued start (F).test_workflow_chat_delivery_save_guard.pytest_workflow_chat_delivery_loop.pystart_background_task_threadsregisters the loop, the workflow scheduler loop does no delivery work, and the real lock helpers gate the real sweep.test_workflow_chat_delivery_placement_and_masking.pytest_workflow_chat_delivery_off_golden.pyb6172962777f).test_workflow_chat_delivery_concurrency.pytest_workflow_chat_delivery_refusals.pyMutations
Each mutation was applied to the production code, its listed delivery tests were run under pytest (normal and
-O), and the file was restored withgit checkout --. M1–M22 come from plan §10, M23–M30 are coordinator amendment 3, and M5b/M7b/M11b/M14b/M17b are extras. Killing tests are shown as<name>::<test>, short fortest_workflow_chat_delivery_<name>.py::<test>.Final full pass: 35/35 killed (normal), 35/35 killed (
-O), with no errors in any run. First full pass: 34/35 (normal) and 34/35 (-O); survivors then: M21.-Oconcurrency::test_claim_race_allows_one_worker_to_deliver_and_the_other_to_stay_busyworker::test_a_create_whose_reply_was_lost_is_found_on_the_retrysave_guard::test_guarded_stale_save_preserves_delivery_and_sweep_stays_exactly_onceworker::test_a_resumed_run_delivers_its_new_outcome_after_the_failed_noteworker::test_delivers_a_composed_reply_once_with_one_unread_mark_and_one_noticeworker::test_a_chat_that_cannot_take_the_delivery_gets_one_notice_instead[removed]worker::test_a_failed_unread_mark_retries_with_the_earlier_planned_timerefusals::test_retry_and_edit_refuse_every_message_a_workflow_run_postedrefusals::test_retry_and_edit_refuse_every_message_a_workflow_run_postedworker::test_delivers_a_composed_reply_once_with_one_unread_mark_and_one_noticeworker::test_delivers_a_composed_reply_once_with_one_unread_mark_and_one_noticeworker::test_delivers_a_composed_reply_once_with_one_unread_mark_and_one_noticestatus_route::test_query_shape_is_single_partition_and_projection_only_in_both_modesstatus_route::test_query_shape_is_single_partition_and_projection_only_in_both_modesstatus_route::test_query_shape_is_single_partition_and_projection_only_in_both_modesstatus_route::test_route_maps_status_errors_and_generic_errors_without_secret_textprojection_and_seed::test_projection_survives_broken_hint_queue_and_direct_signal_returns_falseprojection_and_seed::test_projection_survives_reconcile_failure_and_still_writes_runtimeworker::test_a_chat_that_stays_busy_gets_the_delivery_after_fifteen_minutesworker::test_the_sweep_sends_the_expired_notice_for_a_run_still_running_past_its_windowoff_golden::test_starting_a_workflow_with_results_in_chat_off_is_byte_identical_to_the_base[answer_and_run-absent]off_golden::test_starting_a_workflow_with_results_in_chat_off_is_byte_identical_to_the_base[answer_and_run-absent]worker::test_unreadable_chat_activity_backs_off_then_gives_up_with_one_noticeworker::test_falls_back_to_the_start_of_the_result_when_neither_model_answersworker::test_a_chat_that_cannot_take_the_delivery_gets_one_notice_instead[another_owner]notice_and_unread::test_guarded_unread_write_is_conditional_on_the_etag_it_readoff_golden::test_starting_a_workflow_with_results_in_chat_off_is_byte_identical_to_the_base[answer_and_run-absent]worker::test_a_run_deleted_after_delivery_stays_delivered[tombstone]worker::test_delivers_a_composed_reply_once_with_one_unread_mark_and_one_noticestatus_route::test_skipped_and_cancelled_are_never_retryable_even_with_can_resume[skipped]worker::test_the_sweep_queries_nothing_without_the_lockworker::test_a_create_that_loses_a_race_keeps_the_message_already_postedstatus_route::test_query_shape_is_single_partition_and_projection_only_in_both_modesworker::test_delivers_a_composed_reply_once_with_one_unread_mark_and_one_noticeworker::test_delivers_a_composed_reply_once_with_one_unread_mark_and_one_noticeᴬ Added by coordinator amendment 3.
¹ M21 survived the first full pass in both modes (0F/0E normal, 0F/0E
-O); 70f6cfa addedtest_guarded_unread_write_is_conditional_on_the_etag_it_read, which kills it.Harness.
mutate.pyhandles one mutation at a time:python [-O] -m pytest … -q(COLUMNS=4000, 900 s limit).git checkout -- <file>in afinally, then confirms the tree is clean.Coverage of the final code. The final full pass ran on the final production code. The last
application/change is74f420870; every later commit touches only tests and docs. M1 and M15 were re-run afterdecb3034creworded their kill messages.Status-route contract. The tightened check in
766fea19dwas also mutation-checked. Droppingworkflow_scopefrom the row (functions_workflow_chat_delivery_status.pyL577) is killed in both modes with "row keys changed (6b-2 contract): ['workflow_scope']".Review survivors
The coordinator's pass (149 mutants under pytest and
-Opytest, at766fea19d) left 17 survivors and flagged one partly vacuous test (F). Commits573bd34c8,3cd04b4cf,16ada4f5e,b676db476and447a038b3add tests only. I reapplied each mutation atedd748417with the same exact-anchor harness (mutate_review.py) and ran its target file: 18/18 killed in both modes, with no errors.-Otest_an_exhausted_abandoned_claim_closes_undeliverable_without_composingtest_a_chat_lost_during_the_unread_mark_closes_undeliverable[removed]test_a_workflow_deleted_during_result_read_closes_silentlytest_delivered_message_is_stamped_after_newer_assistant_messagestest_delivery_log_swallows_logging_failurestest_reconcile_pending_terminal_state_is_due_now_even_after_deferraltest_merge_stored_run_fields_drops_incoming_delivery_when_store_has_nonetest_delivery_message_id_requires_delivered_status_and_prefixtest_failed_unknown_gate_reason_maps_to_generic_failure_sentencetest_retry_blocks_foreign_and_group_workflow_documents_as_deletedtest_start_does_not_flag_completed_already_started_run_for_deliverytest_start_requires_queued_run_delivery_record_to_apply_before_flaggingtest_workflow_run_note_uses_delivery_follow_up_textstest_projection_drops_result_chat_delivery_when_stored_run_has_nonetest_guarded_replace_uses_read_etag_so_real_conflict_preserves_concurrent_deliverytest_guarded_unread_read_since_equal_boundary_skips_without_writingtest_guarded_unread_require_private_false_rejects_wrong_owner_without_writingtest_missing_ids_are_not_applicable[-run-delivery-1]ᴱ Optional item E. M20a is equivalent (the real
conversation_is_privatealready rejects a non-owner) and AfterCreate is defensive (every_Undeliverableraise precedes the message create); neither got a test.F. The projection test's fake
queue_runreadoptions["chat_delivery"], so with the gate off it raisedKeyError,_startreturned unavailable, and the off-case checks passed whatever the code did. It now usesoptions.get, finalizes only when the option is present, and requires a queued start.Guardrails
run_v2_guardrails.pyran over1524eeb7b...543f1a5a8, the V2 tip this branch merged: 37 changed files, 12 of them application.py(2 route files), no TS/TSX.single_appfiles compile)The malicious-PR review's "Needs investigation" verdict comes from keyword heuristics: 1,421 findings, no blockers (breakdown from
edd748417: 1,420).try:/except,log_event(, testrequire/assert,# noqaimports, role normalization andskip_if_read_since. None loosens a check.SECRET-RUN-OUTPUT-TEXT,secret-query-text) proving run output and exception text never reach the status payload or logs. No credential is read or added.getattr, fixedre.compilepatterns,importlib.import_moduleof in-repo names, testmonkeypatch.setattr.subprocess.run([sys.executable, …])probes, nevershell=True. The onlyexec(test_workflow_chat_delivery_loop.pyL62–63) runs the realbackground_tasks.pyfunctions, parsed withast, in a seeded namespace, the established harness pattern.urlencodefor the in-app notice link, tests that patchsocket.connectto block the network, and the words "requests" and "telemetry" in fakes.CodeQL / CI
CI on the head
543f1a5a8: 10 checks passed; CodeQL ran before the Python and JS/TS uploads and is being refreshed. The table is fromedd748417.Release Notes Check runs only for PRs into
Development, so it didn't run.Earlier follow-up
0ad92d9db. It cleared three CodeQL alerts in test code from766fea19d(twopy/import-of-mutable-attribute, nowunittest.mock.patch; onepy/catch-base-exception, nowException, with M1 and lease-busy still killed) and lettest_workflow_chat_delivery_off_golden.pyrun withoutPYTHONPATH.The review-survivor tests patch real modules only through
monkeypatch, catch noBaseException, define no closures in loops, and store attributes only on test fakes.Residual risks
chat_delivery. Then nothing is posted, and the status route reports the run asnot_applicable.deadline_at. Their expiry is computed from the admitted deadline, and a run finishing afterexpires_atgets a notice rather than a post.chat_deliveryrecord: roles, model IDs, lease IDs and codes, but never result text.Deviations from the approved plan
All are documented in the feature doc's Deviations from the plan:
skip_if_read_sinceis kept for retries after a read, and the guarded unread mark requires all four ids.available: falsestill returns stored rows, so in-flight cards don't vanish mid-run.runtime/resume(the controlversion), with the A1 reopen ruleversion > generation and not deleted. Expired records never reopen.undeliverablewithdelivery_failed, or asdeliveredwithout a notice if the message already exists. No outcome was read, so there's nothing to tell.deliveredplus the chat notice. If the kind is an expiry, it'sexpiredplus the expired notice. Otherwise it'sundeliverablewithdelivery_failed, plus the undeliverable notice. It never posts a second message.{error, code}with 400. The status route reportscancellingandwaiting_recoveryas running, and falls back from an unknown state to the stored status, then torunning.step_labelis alwaysnull. The workflow-read cap is 20, separate from the 10 live reads.expiredapplies only to non-terminal rows. Timestamps have seconds precision. Approve needs agate_id. An invalid user id returns 503, and an emptyconversation_id=returns 400./workflow-activity?workflowId=…&runId=…&scope=personalpath, with precedent infunctions_m365_runtime.pyL582–589. The V2 bell shows the genericNotificationlabel forworkflow_chat_deliveryuntil 6b-2 adds a case.functional_tests/test_orchestration_workflow_run_adapter.py(+2 lines) addsfunctions_settings: {is_chat_workflow_results_enabled_for_user}to the Phase 5 module's allowed lazy-import set.Step 9 overlap
functions_orchestration_workflow_context.py,functions_orchestration_execution.py,functions_orchestration_workflow_results.py, planner/schema/adapters/composition/executor/recovery/services,admin_settings_fields.py,chat-orchestration.html,workflow.htmlandOrchestrationRunView.tsx. Inroute_backend_orchestration.py, the only edits areenabled_requiredandworkflow_user_requiredadded to the existingfrom functions_settings import …line, a new import of the status module, and the new route after the links route. Thefunctions_orchestration_workflow_contextimport block (L133–141) is untouched.workflow_run_settings_gateis imported infunctions_workflow_chat_delivery_status.py, not the route file.8a802f196, which now holds step 9's doc rows, every doc hunk is still insertion-only (@@ -N,0 +…; 914 insertions, 0 deletions), so step 9's lines are unchanged.orchestration.md: +7 after L296 (after the table) and +1 troubleshooting row after L566.workflow.md: +7 after L67 and +1 after L276.CHAT_WORKFLOW_RESULTS_FOLLOW_UP.md: +3 after L730.edd748417merges8a802f196withgit -c rerere.enabled=false merge. There were two conflicts.config.pykeepsVERSION = "0.261.218", andrelease_notes.mdhas the .218 section on top, then step 9's whole .217 section.route_backend_orchestration.pyand the three other shared docs auto-merged; against V2, the route file is +45/−1, the −1 being thefrom functions_settings import …line.build_docs_inventory.pyregenerateddocs/_data/app_surface.ymlafter the merge, with no diff. Then V2 chat: inline media players, image cards, agent-posted markdown, and interactive maps (0.261.224) #1612 (with Fix group uploads, Call agent saves, scheduled-run identity and GPT-6 model handling #1608) and Fix V2 New chat carrying over a busy conversation's state #1620 landed:35ce8ed58and389c9b14bmerge16d4734adand1524eeb7bthe same way (onlyconfig.pyandrelease_notes.mdconflicted; this branch's notes section stays on top, V2's are unchanged), andaa7a647b9and543f1a5a8renumber this branch's 51 version lines in 21 files,.218→.226→.227, as V2 reached.225and then.226. No inventory diff.docs/_data/app_surface.ymlafter the merge.Follow-ups
mark_conversation_unread_guarded. They're out of scope here.workflow_chat_delivery, and hiding Retry/Edit on notes viametadata.workflow_delivery.functions_personal_workflows→functions_document_actions→functions_document_analysis→functions_search→functions_content→functions_settings→functions_document_actions. Production is unaffected becauseapp.pyimportsconfigandfunctions_settingsfirst; the imports test bootstraps the same way. A follow-up issue is suggested.