Skip to content

Phase 6a: workflow results reader and Follow up in chat - #1592

Merged
Paul Lizer (paullizer) merged 27 commits into
microsoft:paullizer-react-v2-uifrom
paullizer:paullizer-phase-6a-workflow-results-in-chat
Oct 1, 2026
Merged

Paul Lizer (paullizer) merged 27 commits into
microsoft:paullizer-react-v2-uifrom
paullizer:paullizer-phase-6a-workflow-results-in-chat

Conversation

@paullizer

@paullizer Paul Lizer (paullizer) commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Part of #1546
Part of #1543

Draft until the coordinator's review passes. Version 0.261.214 (config.py .213 → .214 only).

This is Phase 6a's first PR: the reader, Follow up, masking on read and the V2 entry points. The workflow_results orchestration capability (step 9 of the plan) is a separate PR after this one merges. It builds on this reader and on Phase 5's workflow planning context, which is now on V2. Nothing from it is on this branch.

Changes since 47187e418 (→ badfff8aa)

At the user's request, relayed by the coordinator, this PR now also fixes CodeQL's py/cyclic-import note (decision 22). The version stays 0.261.214. The body wasn't updated for the 47187e418 round, so the next section covers it.

  • 11b04932f moves conversation_is_private into functions_orchestration_memory.py, unchanged, right after validate_memory_audience, which it calls. A 2-line comment says why it lives there.
    • The planning context imports it at L37, in place of its import of OrchestrationMemoryError and validate_memory_audience. Its own definition is gone, and its two uses (L813 and L1067) are unchanged.
    • Proposals (L56) and run links (L41) still import it through the planning context, so they get the same function object.
    • Saved analysis (L486) and Follow up (L90) import it lazily from the memory module, which imports neither of them. Neither imports the planning context any more.
    • A new 11-test file, test_workflow_result_privacy_import_cycle.py, pins the move, and its mutations M-a to M-f are killed. The feature doc gains a memory-module row and a test row.
  • f88e298f9 merges V2 at 70c255508 (Roadmap status: Phase 3 complete, Phase 5 done, 6a in review #1595, which changes only the roadmap doc), with rerere off and no conflicts.
  • badfff8aa makes the feature doc's memory-module row name the import cycle the move removes.
  • CodeQL reports no new alerts. The bot resolved the thread for code-scanning alert 3919, and I replied there. Everything under Validation was rerun in this round (see "What ran where").

Changes since abc7f6a14 (→ 47187e418)

V2 moved twice: Phase 5 (#1594) and Phase 3c (#1593) merged. I merged V2 into this branch after each, with rerere off. 3c took 0.261.213, so this PR is now 0.261.214.

Apart from version stamps and the step-9 wording, this PR's own changes are the same as at abc7f6a14. For each of the 62 files, its diff against the new base 677c8f3e6 has the same added and removed lines as its diff against 4f778386b. The only other difference is app_surface.yml's capability count, which moved with the base.

  • f8763a09c merges V2 at 5fc50a593 (Phase 5).
    • Conflicts were only in config.py, release_notes.md and docs/reference/chat-controls.md.
      • The release notes keep this PR's section on top. Phase 5's .212 section and the older sections are byte-identical to V2.
      • In chat controls, Phase 5's "Workflow runs (V2 interface)" section comes first, then this PR's "Workflow results in chat (V2 interface)".
    • docs/_data/app_surface.yml was regenerated: 123 capabilities on each side, 124 merged.
  • 30b3fb08b renumbers .213 → .214, mechanically and before the 3c merge. It changes config.py, the release-notes heading, the docs' version and front-matter lines, the test headers, the assert_app_version_at_least floors and the Playwright bootstrap seed.
  • 477d1985f updates the step-9 wording. The feature doc now says step 9 builds on Phase 5's workflow planning context (0.261.212) and on this reader, and ships in a later pull request.
  • 47187e418 merges V2 at 677c8f3e6 (Phase 3c).
    • Conflicts were only in config.py, docs/admin/workflow.md and release_notes.md.
      • The admin page keeps 3c's "Enable AI Workflow Assistant" row, with "Use Workflow Results In Chat" right after it.
      • The release notes keep this PR's .214 section on top. 3c's .213, Phase 5's .212 and the older sections are byte-identical to V2.
    • app_surface.yml didn't change. The merged tree, b7d1880be, matches the coordinator's trial merge.
  • At the time, everything under Validation was rerun on 47187e418 against 677c8f3e6, including Phase 5's and 3c's suites, the mutations and the guardrails. Validation below now covers the current head.

Changes since re-review (97355bc1e → abc7f6a14)

The re-review had one blocking item, B7, and an optional doc nit, and accepted decision 17.

  • B7. The group route's personal fallback clears the summary too (7763cb130).
    • validate_memory_audience counts a personal-store chat as personal when it's group-single-user or has a primary group context, so Follow up is allowed in it. The from-group route converts that chat through the personal fallback, which already withheld the copies but copied the summary as stored and never cleared the source's.
    • _carry_summary_into_collaboration now decides the summary for both conversions, after prepare_m365_history_publication (decision 19).
    • Real-route tests in test_workflow_result_review_paths.py cover the copies, the preview, both summaries, a pending invitee and an ensure-then-sync round trip, with a negative control. B7a and B7b are killed.
  • Doc nit (abc7f6a14). The docs now say "the chat's saved summary is cleared on both chats".

Changes since review (88106dfa5 → 97355bc1e)

The review had one nit and six blocking items, B1–B6. Each fix has a mutation that a test kills (see Mutations).

  • Nit. FollowUpServices.__post_init__ checks each helper with is None, so a falsy injected fake is kept.
  • B1. Collaboration copies are stored withheld.
    • withhold_workflow_result_message lives in the new leaf module functions_workflow_result_masking.py (decision 20). Read-time masking and collaboration storage both call it.
    • Conversion withholds every message that uses a workflow result before building its copy. This is unconditional, because the source is marked converted only in the final write.
    • The withheld form keeps message_kind, reply_to_message_id and sender, and the metadata's sender, source_message_id and source_role, so the copy still builds a valid collaboration doc.
    • The copies come from publication.messages, so the M365 path is withheld too. The summary is cleared on both chats (decision 19), and two more collaboration writes withhold first (decision 18).
  • B2. Single-message export calls authorize_saved_analysis_message_read, with loaders that reuse the message and the chat it already read.
  • B3. The server refuses Retry and Edit of a Follow up turn with 400 workflow_result_retry_unsupported, before content screening or any write (decisions 9 and 17).
  • B4. The listed asserts capture each result first, so python -O can't drop a call.
  • B5. New tests kill R10 (decision 21), R27 and R30.
  • B6. Each request draws a fence code with secrets.token_hex(8), and only markers carrying it delimit the result.
  • Docs and tests. The docs describe the withheld copies and the owner-only raw reads. The offline app boot moved to test_support/workflow_result_offline_app.py, subprocess output is decoded as UTF-8, and the harness imports uuid as a module (97355bc1e).

Summary

With the new admin setting Use Workflow Results In Chat (enable_chat_workflow_results, off by default), a user can ask chat about the stored result of one of their finished personal workflow runs. The workflow isn't run again.

Entry points (V2)

  • Ask in chat on a completed run in a personal workflow's run history, and Ask about this on a personal workflow alert that names a finished run, open a new chat with that run selected.
  • The composer shows "Answering from the Weekly digest run of Mon, Jun 2, 9:02 AM — not re-running the workflow", in the user's locale and time zone, with a remove button.
  • While the run is selected, other sources and Orchestrate are off, and the placeholder reads "Ask about the workflow results…".
  • A second question in the same chat inherits the run without selecting it again.

The reader (functions_workflow_result_reader.py, Flask-free)

  • Owner only. It point-reads the workflow and the run in the requester's /user_id partition. Then it checks the documents' owner and workflow_id, and that neither is a group item. A missing run, someone else's run and a run of another workflow all read as the same non-disclosing not-found.
  • Which runs. completed and completed_partial are readable. Other runs get a closed reason:
    • other terminal runs: workflow_result_not_finished
    • unfinished runs: workflow_result_in_progress
    • older, preview-only runs: workflow_result_preview_only
    • structured (v3) runs: workflow_result_unsupported
  • What "the result" is. Every succeeded task's authoritative output, in run order. They come from one single-partition items query with an exact projection, capped at 200 rows.
  • The snapshot SHA-256. Canonical JSON over the version, the workflow and run ids, the run status, and the ordered (task_id, authoritative_output, result_ref.sha256) of every included output. It doesn't depend on the excerpt budget, and rewriting any included result changes it.
  • Authorization. authorize_workflow_run_read(..., reader_user_id=user_id, result_items=<the digest's rows>) runs with one memoizing manifest loader, so the digest covers exactly what was authorized. Every included manifest must then match its row and pass _require_completed_result. Partial manifests pass only on a completed_partial run.
  • Excerpts (Follow up only; gotcha 40).
    • At most 48 KB of text from at most 8 outputs.
    • The final task's output comes first, with up to half the budget, and every output gets at least 512 B.
    • A cut output gets [Excerpt truncated: the first N KB of M KB.] inside the fence.
    • When the selected model's context budget refuses the excerpt, it's read again at 24, 12 and then 6 KB.
  • Analyze outputs are read through load_workflow_task_input(bounded=True) as SavedAnalysisInputs, and explained the way Ask about this analysis explains them. A run with both kinds is answered from its analyses only, and the answer says the other outputs were left out.
  • What leaves the server. Only the public descriptor: version: 'workflow-result-v1', workflow_id, run_id, workflow_name, status, completed_at, result_sha256 and available. Excerpts stay server-side, and store references never reach the browser.

The descriptor route

GET /api/user/workflows/<workflow_id>/runs/<run_id>/result-context, which the chip uses:

  • requester-only, behind the setting and the personal workflow gate
  • answers with Cache-Control: no-store, private

Follow up (functions_workflow_result_followup.py)

route_backend_chats.py gets a nested wrapper and thin dispatch at the five analysis_result_context sites.

  • The request. It carries workflow_result_context: {workflow_id, run_id, result_sha256}. Sending a saved analysis as well is 400.

  • Private chats only. conversation_is_private runs on every turn, and again before the answer is saved. It's Phase 4's rule, now kept in functions_orchestration_memory.py (decision 22). Shared, collaborative and converted chats are refused. A missing conversation id creates a new personal chat.

  • Bound to the SHA. Every turn reads the result with the context's SHA.

    • A mismatch is 409 workflow_result_changed, and nothing is answered from a different result.
    • Before the answer is saved, the current context and every inherited one are authorized again against their SHAs.
  • Untrusted data (gotcha 41). The server builds every model message itself, in this order:

    1. a fixed system message saying the fenced block is untrusted data from an earlier run, not instructions, and that only markers carrying this request's code delimit the result
    2. the fenced excerpts, between <<<WORKFLOW RESULT <code> (untrusted data)>>> and <<<END WORKFLOW RESULT <code>>>>
    3. bounded history
    4. the question

    The code is a new secrets.token_hex(8) for each request, so a run's output can't know it in advance. Runs of three or more < or > inside the block are still replaced. A look-alike marker that survives that, such as one with full-width brackets or split by a zero-width space, lacks the code, so the system message tells the model it's part of the data.

    The request's own source flags can't add documents, web results or tools.

  • The model call goes through _invoke_saved_analysis_chat_reply: the selected model or local agent, tools off, inside the context budget. Content screening on the question and the answer, the unread marker and usage logging work as they do for saved analyses.

  • Disclosure. Every answer ends with a server-written line, for example "This answer uses the stored result of the Weekly digest run of Mon Jun 2, 2025, 9:02 AM CDT. The workflow was not re-run." The line also says so when the run completed partially, when only part of the result fit, or when outputs were left out. The workflow name is markdown-escaped.

  • Lineage. The answer carries the public workflow_result descriptor and the accumulated workflow_result_contexts. Later answers in the chat inherit the contexts, as they do for saved analyses.

  • Refusals return {error, code, warning_type: 'workflow_result_unavailable', ...} with fixed wording. If a turn fails after the answer was saved, the answer is deleted.

  • Retry and Edit. The server refuses both on a Follow up turn with 400 workflow_result_retry_unsupported, before content screening or any write. Retry checks the retried message and the question that opened its thread; Edit checks the edited message. A later ordinary answer that only inherited the lineage still retries as an ordinary turn.

Masking on read

sanitize_saved_analysis_messages now also recognizes workflow_result, workflow_result_context and workflow_result_contexts, so every read site that already withholds saved analyses is covered at once.

  • Re-authorization. Each distinct context is checked once per call, authorization only, with no excerpts read.
  • When a message shows. Only in its owner's own private personal chat, and only while every context still authorizes. Anything else masks it: a deleted run, lost source access, a changed result or a storage failure. Masking never raises out of a read route.
  • The masked shape mirrors saved analysis: fixed unavailable text, no citations or thoughts, and a trimmed descriptor with available: false. A question keeps its text and loses only its workflow keys. Both forms come from withhold_workflow_result_message in the new leaf module functions_workflow_result_masking.py, which has no application imports.
  • Search. Conversation search skips its cache when a match uses a workflow result.
  • Single-message export. Word, the email draft, and the PowerPoint and generated-file paths load their message through _load_export_message_for_user, which now calls authorize_saved_analysis_message_read and so checks both lineages. Whole-conversation export already ran the sanitizer.
  • Collaboration copies. Several collaboration reads never run the sanitizer (the metadata route, pending invitees included; collaboration summaries; reply previews; MCP collaboration reads), so conversion stores the copies withheld:
    • _copy_legacy_personal_messages_to_collaboration withholds every message that uses a workflow result before building the copy, so last_message_preview is built from the withheld form. This includes the M365 publication's messages.
    • The collaboration's summary is stored as None with the key present, and the source chat's summary is cleared in the final source write.
    • build_collaboration_message_metadata_payload and mirror_source_message_to_collaboration withhold a source message first.
  • Owner-only raw reads. A few older reads that only the chat's owner can reach don't run the sanitizer, as for saved analysis. See Known limitations.

Orchestration history

normalize_history_message skips Follow up answers and answers that inherited their context, until step 9 lets orchestration re-check a result itself.

The 6b seam

lib/workflowResultFollowUp.ts exports followUpWorkflowResult(descriptor, navigation) and openWorkflowResultInChat(workflowId, runId, navigation).

  • Ask in chat and Ask about this already use them.
  • The run's descriptor is always read again rather than trusted from the caller.

Nothing changes with the setting off

  • The descriptor route and every Follow up path refuse with 403 workflow_results_disabled.
  • The bootstrap flag features.enable_chat_workflow_results is false, so V2 shows no buttons and no chip.
  • Masking does nothing for conversations without workflow-result metadata.

Docs

  • docs/explanation/features/CHAT_WORKFLOW_RESULTS_FOLLOW_UP.md
  • the new guide docs/guides/ask-about-workflow-results.md
  • docs/admin/workflow.md and docs/reference/chat-controls.md
  • cross-links in CHAT_ORCHESTRATION_WORKFLOW_PROPOSALS.md, V2_WORKFLOW_ALERT_NOTICES.md, manage-notifications.md and trigger-a-workflow.md
  • the docs-site feature entry
  • the v0.261.214 release notes
  • the regenerated docs/_data/app_surface.yml

Not in this PR

  • Step 9, the workflow_results planner capability. It's the next PR, built on this reader and on Phase 5's generalized planning context, per-capability allowlist and registry fields, which are now on V2. The same setting will gate it (dormant_unless_setting).
  • Phase 6b: post-back delivery, the run card, the recurring-workflow card, the chat-list indicator and v2WorkflowRunPath.
  • Phase 6c: the in-plan wait.
  • Other: group workflows (Phase 8), a retention setting (§9), and re-running a workflow from the chip.

Files

Line counts are against V2 at 70c255508, over 65 files. Apart from decision 22's entries, they haven't changed since abc7f6a14.

New

  • Server:
    • functions_workflow_result_reader.py and functions_workflow_result_followup.py
    • functions_workflow_result_masking.py (68 lines): the leaf module with the message keys, message_uses_workflow_result and withhold_workflow_result_message. It imports nothing from the app, so the collaboration, export and saved-analysis modules can share the withheld form without a new import cycle.
  • V2:
    • lib/workflowResults.ts: parsing, the request helper, latestWorkflowResult and the wording
    • lib/workflowResultFollowUp.ts: the 6b seam
    • components/chat/WorkflowResultChip.tsx
    • components/workflows/WorkflowRunAskInChat.tsx
  • Tests: see Validation. test_support/workflow_result_offline_app.py is the shared offline boot of the real app, used by the chat-routes and review-paths suites. test_workflow_result_privacy_import_cycle.py pins decision 22, with cold-import probes that reuse the existing test_support/offline_bootstrap.py.
  • Docs: see above

Changed, server

  • route_backend_chats.py (+115 −4): the nested wrapper, dispatch and prechecks at the five sites, and inherited contexts in the history helpers
  • functions_saved_analysis.py (+111 −2): masking on read, taking the withheld form from the leaf module, and the privacy check from the memory module
  • functions_collaboration.py (+29 −2): withheld copies on conversion and on mirroring, the withheld source in the message-metadata payload, and the summary cleared on both chats by both conversions, through _carry_summary_into_collaboration
  • route_backend_conversation_export.py (+6 −4): single-message export checks both result lineages
  • route_backend_conversations.py (+20): the search cache, and the Retry and Edit refusals
  • route_backend_workflows.py (+30): the descriptor route
  • functions_settings.py (+31): the default, the update_settings guard, the gate is_chat_workflow_results_enabled_for_user and the decorator workflow_results_required
  • admin_settings_fields.py (+14), route_frontend_admin_settings.py (+3) and templates/admin/_panes/workflow.html (+11): the V2 and classic admin switches
  • route_backend_v2.py (+5): the per-user bootstrap flag
  • functions_orchestration_context.py (+4): the history exclusion
  • functions_orchestration_memory.py (+16 −1): conversation_is_private, moved here unchanged (decision 22)
  • functions_orchestration_workflow_context.py (+2 −15): imports it from the memory module instead of defining it (decision 22)
  • config.py: the version

Changed, V2

  • stores/chatStore.ts (+174 −2)
  • components/chat/Composer.tsx (+31 −8)
  • pages/ChatPage.tsx (+30 −3)
  • lib/conversationUrl.ts (+56 −1): the one-shot result_workflow_id and result_run_id parameters, read on the first render and stripped by syncedConversationParams
  • lib/types.ts (+20)
  • lib/workflowAlertNotices.ts (+27 −5): workflowAlertFollowUpAction
  • components/notifications/WorkflowAlertCard.tsx (+8 −1): passes { enabled, open } to it
  • components/workflows/WorkflowRunHistory.tsx (+2)

Overlap with Phase 5 and Phase 3c

Phase 5 changed 13 of this PR's 65 files, and 3c changed 4 of them:

  • both phases: config.py, docs/_data/features.yml, docs/admin/workflow.md and docs/explanation/release_notes.md
  • Phase 5, server: admin_settings_fields.py, functions_orchestration_workflow_context.py, functions_settings.py and route_frontend_admin_settings.py
  • Phase 5, docs: docs/_data/app_surface.yml, CHAT_ORCHESTRATION_WORKFLOW_PROPOSALS.md, trigger-a-workflow.md and docs/reference/chat-controls.md
  • Phase 5, test: route_tests/test_route_blueprint_policy_inventory.py

Phase 5's enable_chat_orchestration_workflow_runs and this PR's enable_chat_workflow_results sit in separate hunks, and both are kept. Only 4 files conflicted: config.py, release_notes.md, chat-controls.md and docs/admin/workflow.md.

In functions_orchestration_workflow_context.py, this PR changes only what decision 22 needs: the L37 import, the removed definition and the docstring version. Phase 5's gate, context builder, per-capability allowlist and quota are untouched. No other application file this PR adds or changes imports the planning context.

Neither phase changed WorkflowRunHistory.tsx (+2 here: one import and one <WorkflowRunAskInChat … /> in the run row), workflow.html, route_backend_v2.py, functions_orchestration_context.py or functions_orchestration_memory.py.

The rest of Phase 5's original file list is still untouched here:

  • the registry, planner, schema and plan handling
  • RunContext, the adapters and route_backend_orchestration.py
  • MessageList.tsx, OrchestrationPlanCard.tsx, orchestrationController.ts and workflowRunLink.ts

Decisions made autonomously

From the approved plan

  1. One setting. enable_chat_workflow_results, in the Workflow tab, off by default, as the roadmap recommends.
    • It gates Follow up, Ask in chat, Ask about this, and step 9's capability.
    • It needs allow_user_workflows and follows require_member_of_workflow_user. It doesn't need enable_chat_orchestration.
  2. Readable means completed and completed_partial.
  3. The result is every succeeded task's authoritative output, the final task first under the budget. Structured (v3) runs are closed with workflow_result_unsupported, and named outputs come later.
  4. The SHA covers the ids, the run status and (task_id, authoritative_output, result_ref.sha256) for every included task. It's taken from the same rows that authorization checks.
  5. The budget is 48 KB in total and at most 8 outputs, stepping down to 6 KB. A truncation note goes in the fence and in the disclosure.
  6. Mixed runs (Analyze outputs plus other outputs) are answered from the analyses only, and the answer says so. Saved-analysis answers are checked against their records (_report_numbers_supported), so text excerpts can't ride along in the same call.
  7. Masking re-checks result access, not the feature entitlement, as it does for saved analyses that workflows produce.
    • Turning the setting off or losing the WorkflowUser role stops new questions, but doesn't hide earlier answers.
    • Every exception masks.
  8. Ask in chat and Ask about this open a new chat. Ask about this appears only on personal, non-failure alerts that name a run.
  9. Retry and Edit.
    • The server refuses a retry or edit of a Follow up question or answer with 400 workflow_result_retry_unsupported, right after the ownership checks and before content screening or any write. Retry checks the retried message and the question that opened its thread. Edit checks the edited message.
    • A chat request that carries a workflow_result_context together with a retry or edit id gets the same refusal.
    • V2 also refuses Retry and Edit on a Follow up question or answer before calling the server, and asks the user to ask again.
    • A later ordinary answer that only inherited the run's context retries as an ordinary turn (decision 17).
  10. The client sends time_zone, so the disclosure shows the user's local time. An invalid zone falls back to UTC.
  11. Masking also enforces privacy on read.
    • Follow up answers, and answers that inherited them, show only in the reader's own private personal chat.
    • Converting a chat to a collaboration therefore hides them for everyone, the owner included (gotcha 8). Conversion copies the messages, and the collaboration bridge feeds the source chat's history to shared replies.
    • The original chat's stored messages are unchanged. The collaboration's copies are stored withheld, and the chat's saved summary is cleared on both chats. So relaxing the rule later would show the original chat's answers again, but not the collaboration's copies or the cleared summary.
  12. The document-action and analyze streams refuse a workflow-result request before creating a conversation or opening a stream, with the same codes. A refused request leaves nothing behind.
  13. Search skips its cache for matches that use a workflow result, as it does for saved analyses.

Made while building

  1. The structured-conversion edge case is documented, not fixed.
    • Two places still offer a structured run: an older run of a workflow that was later converted to structured shows Ask in chat, and a structured workflow's alert shows Ask about this.
    • The server refuses both with workflow_result_unsupported, and the chip says so.
    • Hiding them needs the workflow's definition version in the run history and the alert. That means threading a prop through WorkflowsSection.tsx and WorkflowRunHistory.tsx. WorkflowRunHistory.tsx was on Phase 5's list, so it stayed a follow-up. Phase 5 didn't change that file in the end, and 3c has since changed WorkflowsSection.tsx.
  2. A question sent while Ask in chat is still reading the run is held back with "The workflow result is still opening…". It isn't sent as an ordinary turn a moment before the chip appears.
  3. A partition-blind test pins the owner checks. The reader's in-document owner checks sit behind partition-scoped point reads, so ordinary tests couldn't reach them. The test serves a document from another partition.

Made while addressing the review

  1. The Retry and Edit refusal is narrower than the review asked. It uses message_asks_about_workflow_result (workflow_result or workflow_result_context) instead of message_uses_workflow_result, which also matches workflow_result_contexts.
    • Retry copies metadata only from the thread's first user question, and _analysis_history_metadata() is spread only onto assistant and augmentation messages, so a question never carries workflow_result_contexts.
    • Retrying an answer that only inherited the lineage therefore replays an ordinary question. The new answer re-derives its contexts through the usual history sanitize and is masked with them, so nothing carries a stale context forward.
    • The broad check would block Retry on every later answer in any chat that once used Follow up. V2's turnAsksAboutWorkflowResult uses the same narrow rule.
    • Edit checks only the edited message. Because Retry and Edit are refused on a Follow up thread, it never gets a second attempt, so the only question that can be edited in such a thread is the original one, which carries the context.
    • Mutation B3d (the broad helper on Retry) is killed by a test that retries an inherited-only answer.
  2. Two more collaboration writes withhold first. build_collaboration_message_metadata_payload merges the source message's metadata into the metadata route's payload, and mirror_source_message_to_collaboration copies a later source message into the collaboration. Neither ran the sanitizer, so both now withhold a message that uses a workflow result. Both are no-ops for every other message.
  3. The summary is cleared on both docs. The collaboration doc stores summary: None with the key present. ensure_collaboration_source_conversation pushes the value only when the key exists, so it pushes None onto the source, and a summary regenerated on the hidden source chat is cleared before sync_collaboration_conversation_metadata_from_source copies it back. The source's own summary is cleared in the final source write, after the M365 fingerprint is taken. One helper, _carry_summary_into_collaboration, makes this decision for both the personal route and the group route's personal fallback (B7), so the two can't drift.
  4. A leaf module for the shared shape. functions_workflow_result_masking.py has no application imports. The reader, read-time masking, collaboration storage, chat routes and the conversation routes import the keys, the predicates and withhold_workflow_result_message from it, so collaboration storage doesn't import the reader. Compared with 88106dfa5, every touched module has the same number of import edges that lead into a cycle, and the leaf is in no cycle.
  5. R10's test asserts no cache write, not no cache read. Conversation search builds its key and reads the cache before it matches, so the one read can't be avoided without restructuring the search. After matching, a result that uses a workflow result sets the key to None, so nothing is written. The test pins 1 read (a miss) and 0 writes.

Made at the user's request

  1. The privacy check moved into functions_orchestration_memory.py, which fixes CodeQL's py/cyclic-import note (code-scanning alert 3919).
    • It's one rule, moved unchanged rather than copied. It moved into the memory module rather than a new leaf, because it calls validate_memory_audience and catches OrchestrationMemoryError, both defined there.
    • The planning context imports it, so proposals and run links keep the same function object. Saved analysis and Follow up import it lazily from the memory module and no longer import the planning context, whose imports led back to saved analysis.
    • This doesn't make the modules cycle-free. Apart from that swap, each module's count of import edges that lead into a cycle is unchanged (see Validation).

Errors

The "Chip" column says what V2 does with the selected run when the refusal arrives.

Code Status When Chip
workflow_results_disabled 403 The setting is off, or the personal workflow gate refuses the user. Removed
workflow_result_invalid_context 400 The context isn't two valid ids and a 64-character lowercase hex digest. Removed
workflow_result_private_only 403 The chat is shared, collaborative, converted, or someone else's. Removed
workflow_result_not_found 404 The workflow or run is missing, deleted or someone else's. Removed
workflow_result_access_denied 403 Run authorization failed, for example a source the owner can no longer read. Removed
workflow_result_changed 409 The result's digest differs from the selected one. Removed
workflow_result_in_progress 409 The run hasn't finished. Removed
workflow_result_not_finished 409 The run failed, was cancelled, or ended without completing. Removed
workflow_result_preview_only 409 No task stored a durable result (older runs). Removed
workflow_result_unsupported 409 A structured (version 3) run, or a result made only of combined reports. Removed
workflow_result_invalid 409 The stored result is inconsistent, or the run has more than 200 task rows. Removed
workflow_result_conversation_unavailable 404 The chat no longer exists or was deleted. Removed
workflow_result_context_conflict 400 The request carries both a saved analysis and a workflow result. Kept
workflow_result_retry_unsupported 400 Retry or Edit of a Follow up question or answer, or a chat request that carries a workflow result context with a retry or edit id. Kept
workflow_result_too_large 400 The result and history don't fit the model, even at a 6 KB excerpt. Kept
workflow_result_model_unsupported 400 The selected agent can't answer with its tools off. Kept
workflow_result_answer_rejected 400 A saved-analysis answer didn't pass the checks against its saved records. Kept
workflow_result_storage_unavailable 503 Cosmos DB, the result store, or another retryable check failed. Kept
workflow_result_answer_failed 503 The model call failed, or the chat changed while the answer was written. Kept

Sign-in failures are 401 from the usual decorators, and V2 asks the user to sign in again.

A storage failure is always 503, never "not found" or "no runs". The reader doesn't use the personal getters that swallow errors.

Performance

  • One check of a result happens on the descriptor route, on each Follow up read, on the re-check before saving, and in masking. It costs:

    • 2 point reads (the workflow and the run)
    • 1 single-partition query on the run items
    • one manifest load per included task, plus run authorization's consumed-ancestor loads, shared through one memoizing loader
    • source authorization for any Analyze lineage

    There's no history paging, because structured runs are closed.

  • A Follow up turn reads the result once with excerpts, and once more for each budget step it needs. Then it re-checks the current and every inherited context before saving.

  • Every chat send runs the history sanitize.

    • A conversation that holds a Follow up answer pays, on each turn: one conversation read, plus one run authorization per distinct context, including the manifest loads and the consumed-ancestor loads.
    • Opening such a conversation pays the same once per read.
    • Conversations without Follow up answers pay nothing.
    • Tests pin it: K messages sharing one context cost one run authorization per sanitize call, and no workflow metadata costs zero reads.
  • A single-message export of an answer that uses a workflow result pays one run authorization per distinct context, and reuses the chat it already read for the privacy check. Other messages export as before.

  • Converting a chat only checks each message's metadata keys, with no extra reads.

  • Excerpts are at most 48 KB of text per turn. A large output is read as one bounded page, and a record output as at most 8 pages.

Security and CodeQL notes

  • Logs.
    • Messages are constant, for example [WorkflowResults] Workflow result unavailable, [WorkflowResults] Follow up answer refused and [WorkflowResults] A chat answer's workflow result was withheld on read.
    • extra carries only the code, stage, check and exception type name.
    • Logs never carry workflow names, run output, questions, answers or ids (py/log-injection).
  • Responses use the fixed wording from _REASONS and never echo exception text (py/stack-trace-exposure).
  • Context validation. Ids must match [A-Za-z0-9][A-Za-z0-9_.:-]{0,199}, and the SHA must be 64 lowercase hex characters. The V2 URL parameters use the same pattern.
  • The workflow name is user-authored. It's markdown-escaped in the server-written disclosure, and V2 renders it as React text. A Playwright test checks that a hostile name stays text.
  • The fence. Each request draws a new code (secrets.token_hex(8)) for its markers, and the system message says only markers carrying it delimit the result. Runs of three or more < or > inside the run output are still replaced. A look-alike marker that survives that, such as one with full-width brackets or split by a zero-width space, lacks the code, so the model is told it's part of the data.
  • Store references (result_ref and blob paths) never leave the server.

CodeQL on this PR. At 8a94e4f5d, CodeQL reported 2 new alerts in this PR's lines: 1 error and 1 note.

  • The error, code-scanning alert 3920, is fixed in 88106dfa5, with no behavior change.
    • The alert was py/call/wrong-arguments at functions_workflow_result_followup.py L404.
    • FollowUpServices had six dataclass fields whose defaults were plain functions: serialize, gate, is_private, read_result, authorize_context and now.
    • A dataclass default is also a class attribute, and CodeQL reads a function on the class as a bound method. So services.serialize(user_doc) looked like one argument too many.
    • The calls did work, because the generated __init__ stores the value on the instance.
    • The fields now default to None, and __post_init__ sets the real helpers on the instance, like the module's other optional hooks.
    • A new test fails if a field default is ever a function again (mutation M10).
    • The error's thread was resolved automatically once 88106dfa5 fixed it.
  • The note, code-scanning alert 3919, is fixed in 11b04932f (decision 22).
    • The alert was py/cyclic-import (recommendation) at functions_saved_analysis.py L486, the function-level import of conversation_is_private from the planning context in read-time masking. The planning context led back to functions_saved_analysis through functions_orchestration_registry → functions_orchestration_native_results → functions_native_analysis_results.
    • L486 now imports it lazily from functions_orchestration_memory, so that loop is gone. Follow up's import (L90) moved the same way. The bot resolved the thread, and I replied there.
  • A test-harness warning, code-scanning alert 3933, is fixed in 97355bc1e.
    • At 93af89bab, CodeQL raised py/import-of-mutable-attribute at functional_tests/test_support/workflow_result_offline_app.py L24. The offline boot had moved into that module with a module-level from uuid import uuid4. test_workflow_draft_save_parity.py reassigns uuid.uuid4 (L297 and L304), and a name imported directly doesn't see that.
    • The harness now uses import uuid and uuid.uuid4().hex. It's test-only.
    • The bot's thread was resolved automatically.

At badfff8aa, CodeQL reports no new alerts on this PR.

Validation

Each test file runs in its own pytest process, normally and under python -O, from a Python 3.12 venv. On this machine, Windows' platform._wmi_query can stall a fresh interpreter, so the pytest runs put a session-local sitecustomize.py on PYTHONPATH that makes it raise OSError, and platform falls back. The shim isn't committed and changes no test.

What ran where. Head is badfff8aa.

  • The suites, Playwright, Node, the V2 build and the 31 mutations ran on f88e298f9. badfff8aa changes only one row of the feature doc.
  • The docs checks and the guardrails ran on badfff8aa, against V2 at 70c255508.
  • The review and regression sets compare f88e298f9 with a clean worktree of 677c8f3e6. V2's 70c255508 changes only the roadmap doc on top of it.

CI on badfff8aa. All 11 checks pass, including CodeQL for Python, JavaScript/TypeScript and Actions. CodeQL reports no new alerts.

New suites. Each passes, normally and under python -O.

Suite Passed
test_workflow_result_reader.py 99
test_workflow_result_followup.py 91
test_workflow_result_chat_routes.py (dispatch at every entry point, plus an offline boot of the real app through the real JSON, SSE and history routes) 12
test_workflow_result_review_paths.py (an offline boot of the real app through conversion by the personal route and the group route's personal fallback, the collaboration routes, summary and sync, MCP reads, mirroring, the three single-message exports, Retry, Edit and search) 2
test_workflow_result_masking.py 49
test_workflow_result_privacy_import_cycle.py (decision 22) 11
test_workflow_result_orchestration_lineage.py 11
test_chat_workflow_results_admin.py 15
route_tests/test_workflow_result_context_policy.py 38

Existing suites. 40 more files pass in both modes.

  • Workflow results: test_workflow_result_contract.py 10, test_workflow_result_store.py 38 (and 93 subtests) and test_workflow_result_routes.py 26.
  • Orchestration memory, plans and Ask AI: test_orchestration_memory_context.py 13, test_orchestration_v2_plan_backend.py 61 (and 88 subtests), test_v2_assist_thread.py 14, test_assist_thread_submission_id.py 61 (and 62 subtests), test_v2_workflow_ask_ai.py 7 and test_v2_workflow_ask_ai_parity.py 12.
  • Phases 4 and 5, 21 files. test_orchestration_workflow_*: planning_context 112, propose_capability 88, proposal_routes 60, proposal_imports 6, proposals_admin 12, setting_off_golden 9, proposal_done_when 2, proposal_editor_round_trip 5, deliverable 26, week_preview 14, run_adapter 129, run_answer 18, run_approval_floor 39, run_capability 79, run_effects 10, run_link_routes 63, run_planning_context 80, runs_admin 14, runs_off_golden 5 and run_imports 13. Also test_orchestration_deliverables.py 53.
  • Saved analysis, 7 files. test_saved_analysis_*: artifact_access 11, cleanup 7, diagnostics 32, record_pages 9, routes 13, service 14 and store_integration 26.
  • Route policy: blueprint inventory 10, policy coverage 2 and the unauthenticated contract 5.

Three of the 52 files fail on both trees, the same way.

  • test_v2_admin_workflow_parity.py: 1 failed, 8 passed, in both modes. This PR adds one expected dependency, enable_chat_workflow_results → allow_user_workflows, whose declared gate matches when read directly. The failing test, test_sub_settings_are_gated_by_their_capability, raises AttributeError on group_workflow_allowed_group_ids, whose V2 depends_on is a list. Base fails it the same way.
  • test_saved_analysis_bounded_reports.py: 6 failed, 8 passed, on a stale monkeypatch target in functions_workflow_context.
  • test_saved_analysis_model_metadata.py: 2 failed, on assert None == 12000.

The last two are also in the regression set, where base fails them identically.

Decision 22. test_workflow_result_privacy_import_cycle.py has 11 tests:

  • the version floor
  • two AST checks: saved analysis and Follow up import the check only inside _WorkflowResultLineage._private and _default_is_private, only from the memory module, and import nothing from the planning context
  • the planning context reuses the memory module's function instead of defining its own
  • the memory module imports nothing from saved analysis, Follow up, the reader, masking or the planning context
  • six cold loads in fresh python -B processes, normally and under -O: the memory module alone, the callers first, and the planning context first
    • A private chat is private. A converted chat, one shared through collaboration_conversation_id and someone else's chat aren't.
    • The memory module alone loads none of its callers. The callers check a chat without loading the planning context. The planning context's function is the memory module's object.
    • No process attempts a network call.

The table counts each module's import edges that lead into a cycle. An edge counts when the imported module leads back to the importer through any import statements, lazy ones included.

Module Base 677c8f3e6 47187e418 Head
functions_saved_analysis.py 19 21 21
functions_workflow_result_followup.py new 7 7
functions_orchestration_workflow_context.py 8 8 8
functions_orchestration_memory.py 2 2 2
functions_workflow_result_reader.py new 3 3
functions_workflow_result_masking.py new 0 0
functions_orchestration_workflow_proposals.py 0 0 0
functions_orchestration_workflow_run_links.py 0 0 0
  • Since 47187e418, saved analysis and Follow up have each swapped their planning-context edge for a memory-module edge. Nothing else changed.
  • Against base, saved analysis has 2 more edges, the reader and the memory module. Both are lazy imports this PR adds.
  • Each caller's memory-module edge still counts, because the memory module's lazy functions_fact_memory_context import (L129) leads back to both callers. CodeQL reports no alert for them, but the modules aren't cycle-free.

The six mutations ran on 11b04932f against the file's 11 tests, and all are killed:

Id Mutation Failed
M-a Saved analysis imports the check from the planning context 3: the saved-analysis AST check, and the callers-first cold load, normal and optimized
M-b Follow up imports the check from the planning context 3: the Follow up AST check, and the callers-first cold load, normal and optimized
M-c The planning context defines its own check 5: the reuse check, and the callers-first and planning-context-first cold loads, normal and optimized
M-d The memory module imports masking at module level 1: the imports-nothing check
M-e Saved analysis imports the check at module level 1: the saved-analysis AST check
M-f The memory module lazily loads saved analysis inside the check 3: the imports-nothing check, and the memory-alone cold load, normal and optimized

Review set. The review's fixes change personal and group collaboration conversion and summaries, the collaboration metadata route, MCP collaboration reads, single-message export, and Retry and Edit. I ran the 23 existing test files that cover those paths on f88e298f9 and on 677c8f3e6, normally and under python -O, then compared the failing test ids and their causes.

  • 15 files pass in both modes on both trees, apart from one base run noted below: test_workflow_saved_output_artifacts.py (42), test_conversation_export_mermaid_tex_images.py (18), test_per_message_export.py (16), test_v2_shared_conversations.py (14), test_inbound_mcp_governance_and_tools.py (9), test_message_export_progress_feedback.py (8), test_v2_message_actions.py (7), test_m365_conversation_lifecycle.py (5), test_message_audio_export.py (4), test_chat_retry_edit_streaming_parity.py (4), test_collaborative_conversation_foundation.py (3), test_collaboration_inline_image_proposal_porting_fix.py (3), and test_collaboration_shared_conversation_management_fix.py, test_collaboration_metadata_and_mentions_fix.py and test_m365_collaboration_action_cards.py (1 each).
  • The other 8 fail with the same ids and causes on both trees, in both modes (0 new failures):
File Failing on both Cause
test_group_collaboration_source_storage_fix.py 83, all subtests of 4 tests (14 tests pass) M365PolicyError: History sharing authorization is not configured (59) and NameError: M365ApprovalRequired (24)
test_conversations_read_ownership_authorization.py 9 ImportError: the test's fake config has no cosmos_msgraph_pending_actions_container
test_per_message_powerpoint_export.py 3 (4 pass) exec-namespace NameError: resolve_block_sources_in_content
test_collaboration_conversation_summary_export_fix.py 2 "Expected collaboration metadata update to succeed", and an exact-version assert
test_enhanced_citations_blob_and_collaboration_fix.py 2 missing blob-resolution source snippets, and an exact-version assert
test_collaboration_legacy_message_conversion.py 1 assert 'image' == 'user'
test_scope_lock_collaboration_unlock_fix.py 1 an exact-version assert
test_persistent_conversation_summary.py collection error Cosmos TypeError: Unrecognized credential type

On base under -O, test_m365_conversation_lifecycle.py's test_real_conversation_deletion_fork_copy_and_retention_routes failed once, in a 152 s run. A rerun passed all 5 in 25 s. Head passed all 5 under -O in 23 s.

Regression set. I ran 43 more files normally on f88e298f9 and on 677c8f3e6, and compared failing test ids. They're 13 chat files, 9 saved-analysis, 9 route-policy and 12 from Phase 5, including test_orchestration_dependency_allowlist.py (41) and test_v2_orchestration_workflow_run_links_xss_guardrail.py (4).

  • 33 pass on both trees.
  • Head has 840 passed and 107 failed, and base 839 and 107, each with 12 passing subtests. The extra pass is this PR's test_workflow_result_context_requires_a_signed_in_user_session in the unauthenticated contract (5 tests against 4).
  • The other 10 fail with identical failing sets on both trees (0 new failures):
File Failing on both Cause
test_analyze_backend_saved_integration.py 17 NameErrors (check_chat_content, checked_history_messages), so 503 instead of 200, 400 or 403
test_v2_prompt_attachment_persistence.py 72 NameErrors (attach_chat_check, DEPENDENCY_PLAN_CONTRACT_VERSION, prepared_chat_image_references), so 500
test_saved_analysis_bounded_reports.py 6 a stale monkeypatch target
test_chat_stream_history_context_fix.py 4 NameError: checked_history_messages
test_chat_stream_retry_context_init_fix.py 2 a missing fix doc under docs/explanation/fixes, and a source-pattern assert about is_retry
test_saved_analysis_model_metadata.py 2 assert None == 12000
test_v2_model_identity_and_scope.py 1 a source-snippet assert on selection_key
test_route_backend_chats_redundant_assignment.py 1 an exact-version assert (0.239.148)
test_chat_route_unreachable_code_cleanup.py 1 an exact-version assert (0.250.116)
test_backend_chats_swagger_integration.py 1 Cosmos TypeError: Unrecognized credential type

Other checks

  • Node: all 58 .mjs files under functional_tests ran, one of them the test_support/tsResolve.mjs helper, and 57 exit 0.
    • test_workflow_results_clients.mjs passes its 10 groups. testRetryGuard includes R30's case, a message with workflow_result_context and no thread info.
    • test_saved_analysis_clients.mjs passes.
    • test_v2_workspace_action_authoring_logic.mjs fails with AssertionError: m365_calendar_plugin.py, on base too.
  • V2: npm run build, which runs tsc -b before vite build (2703 modules), passes before and after the mutations.
  • Playwright: 15 files under ui_tests, 266 tests, all pass.
    • test_chat_workflow_results.py 25, test_v2_workflow_alert_notices.py 28, test_v2_orchestration_workflow_run_links.py 29, test_v2_orchestration_workflow_proposal_card.py 22, test_v2_orchestration_plan_editor.py 35 and test_v2_orchestration_dependency_plans.py 32
    • test_v2_workflow_ask_ai.py 32, test_v2_workflow_ask_ai_proposal.py 3, test_v2_assist_thread.py 10 and test_v2_plan_editor_references.py 11
    • test_v2_orchestration_workflow_run_floor.py 9, test_admin_orchestration_workflow_runs.py 9, test_v2_orchestration_streaming_bubble.py 3, test_v2_orchestration_generated_images.py 3 and test_v2_chat_image_uploads.py 15
    • While I wrote the suite, it caught all 5 deliberate client breaks I tried.
  • Docs: build_docs_inventory.py shows no diff. test_docs_app_surface_coverage.py passes 7/7, test_docs_site_quality.py 6/6 and test_docs_latest_release_catalog_parity.py 6/6.

Pre-existing docs failures. None of these are in this PR's lines.

  • test_docs_link_integrity.py: 1 of 4 checks passes, on head and on base.
    • 11 of 1139 relative links are broken (base: 11 of 1128).
    • 9 of 801 site links don't resolve (base: 9 of 788).
    • 2 of 71 features.yml links don't resolve (base: 2 of 68): CONTENT_SCREENING_FRAMEWORK and CHAT_CONTENT_CHECKS.
    • The two in reference/chat-controls.md are at L338 and L349, which this PR doesn't touch.
  • test_docs_release_notes_integrity.py: the generated release-notes pages are stale. They're missing 150 releases, and 149 of those were already missing on base. The other is this PR's v0.261.214. Regenerating them is outside this PR.

Mutations

All 31 server and client mutations are killed on f88e298f9, by the same tests as at abc7f6a14. The runner restores the bytes after each one. Decision 22's M-a to M-f are listed above.

Besides R10, R27 and R30, the reviews asked for eight more:

Requested mutation Id
Copy masking removed B1a
Summary drop removed B1b and B1c, now in the shared helper
Export check removed B2
Retry refusal removed B3a
Edit refusal removed B3c
Nonce removed B6a
Group conversion copies the summary as stored B7a
Group conversion keeps the source summary B7b

In the table, "review paths" means test_workflow_result_review_paths.py::test_review_paths_withhold_copies_exports_replays_and_cached_searches, followed by its failing assertion.

Id Mutation Killed by
M1 Drop the SHA comparison reader test_a_resume_that_rewrites_an_included_result_changes_the_digest_and_nothing_is_read
M2 Drop the privacy check on a follow-up turn followup test_shared_collaborative_converted_and_hidden_chats_are_refused_before_the_result_is_read[shared0]
M3 Drop authorize_workflow_run_read reader test_the_items_query_is_the_exact_projection_and_the_real_authorizer_accepts_its_rows
M4a Drop the workflow owner comparison reader test_each_owner_comparison_holds_if_a_read_ever_loses_its_partition_scope[workflows]
M4b Drop the run owner comparison reader test_each_owner_comparison_holds_if_a_read_ever_loses_its_partition_scope[runs]
M4c Drop the run's workflow_id match reader test_someone_elses_or_a_missing_run_reads_exactly_like_not_found[run_of_another_workflow]
M5 Drop masking on read masking test_a_readable_result_in_the_owners_private_chat_is_shown_unchanged
M5b Drop the privacy check on read masking, the same test
M6 Drop the excerpt budget reader test_one_long_text_output_uses_the_whole_budget_and_says_it_was_cut
M7 The client helper sends web_search_enabled node test_workflow_results_clients.mjs
M8 Drop c.workflow_id from the items projection reader test_a_finished_run_reads_as_its_owner_with_a_public_descriptor
M9 Remove the orchestration history exclusion lineage test_a_follow_up_answer_and_answers_that_inherited_it_stay_out_of_history
M10 Put a function default back on FollowUpServices followup test_omitted_helpers_default_to_the_real_ones_without_becoming_methods
M10b Drop __post_init__, so the helpers stay None followup test_a_question_is_answered_from_the_selected_run_only_with_the_disclosure, and 54 more
R10 Drop workflow results from the search-cache skip review paths: "A search over workflow-result answers was cached"
R27 Drop the reader's check that each manifest names its own task reader test_the_reader_rechecks_that_each_manifest_names_its_own_task
R30 Drop the message's own check in turnAsksAboutWorkflowResult node test_workflow_results_clients.mjs: "found on the message itself"
B1a Copy workflow-result messages into a collaboration as stored review paths: "The conversion response leaked."
B1b Keep the summary on the collaboration, in _carry_summary_into_collaboration review paths: "The conversion response leaked." The response carries the collaboration's summary.
B1c Keep the summary on the hidden source chat, in _carry_summary_into_collaboration review paths: "A summary survived conversion"
B7a Group conversion copies the source summary onto the collaboration as stored (the base behavior) review paths: "The group conversion response leaked."
B7b Group conversion clears only a copy of the source doc, so the stored source keeps its summary review paths: "A summary survived the group conversion"
B1d Drop the source mask in the metadata route's payload review paths: "The metadata read leaked" the workflow id, run id and digest
B1e Drop the mirror mask review paths: "The mirrored answer wasn't withheld"
B2 Single-message export checks saved analyses only (the base behavior) review paths: "After the run was deleted, the Follow up answer: word 200"
B3a Drop the Retry refusal review paths: "retry of" the Follow up answer, 200
B3b Drop Retry's check of the question review paths: "retry of msg-bonds-a: 200". That answer kept no lineage, but its question asked about the result.
B3c Drop the Edit refusal review paths: "edit of" the Follow up question, 200
B3d Also refuse Retry of an answer that only inherited the context (the broad helper) review paths: "The inherited answer's retry wasn't checked: 400 workflow_result_retry_unsupported"
B6a Fence markers without the code followup test_a_question_is_answered_from_the_selected_run_only_with_the_disclosure
B6b One fixed code for every request followup test_the_fence_code_is_drawn_again_for_every_request_and_matches_its_system_message

M4a and M4b first survived. The point reads are partition-scoped, so the fake store never returned another user's document. Commit 8a94e4f5d adds the partition-blind test (decision 16), and both are now killed.

Guardrails on badfff8aa, against V2 at 70c255508, over the 65 changed files. These mirror the CI workflows' file filters, and all pass.

  • broken-access-control: 17 files
  • xss-sinks: 30 files, including the changed V2 .ts and .tsx files
  • swagger-routes: 6 route files
  • python-syntax: 491 files compile
  • Extended runs, all passing:
    • broken-access-control over all 34 changed .py files
    • xss-sinks over 50 changed files
    • xss-sinks with --full-file over the full text of the 12 changed V2 files
  • malicious-pr-review: no blocker findings.
    • It reports 1525 advisory keyword findings (1433 Important, 92 Moderate), each with the verdict "Needs investigation" rather than "Blocker". That's expected for a change at the chat and AI boundary.
    • That's 23 more than the 1502 at abc7f6a14. 25 are new: the memory module (7), the planning context (1), saved analysis (1), Follow up (1), the feature doc (1) and the privacy test (14). The 2 import lines this change replaced are gone, and one finding in the route policy inventory only changed its version from 0.261.213 to 0.261.214.
    • There are no dependency or CI policy findings, and the only hosts are test hosts: example.test, model.invalid and simplechat.test.

Known limitations

  • Personal workflows only.

  • Structured (version 3) runs aren't supported. See decision 14 for the places that still offer them.

  • Authoritative outputs only. Named outputs aren't read.

  • Large results. An answer sees at most 48 KB of excerpts from up to 8 outputs, and says so when it saw only part of the result. A run with more than 200 task rows can't be read.

  • Mixed runs are answered from their saved analyses only.

  • Retry and Edit on a Follow up question or answer are refused, by V2 and by the server, with 400 workflow_result_retry_unsupported. Ask the question again instead. A later answer that only inherited a workflow-result context retries as an ordinary turn, and its history is withheld on the same terms.

  • Output formats such as "make this a CSV" aren't supported in Follow up.

  • Orchestration history omits Follow up answers and later answers that inherited their workflow-result context, until the workflow_results capability ships.

  • Shared and converted chats. Once a chat is shared or converted to a collaboration, its Follow up answers are hidden for everyone, the owner included. The original chat's stored messages are unchanged. The collaboration's copies are stored with these answers withheld, and the chat's saved summary is cleared on both chats.

  • No retention setting. A run's result stays available until the run is deleted.

  • Classic chat has no entry points or chip, and its message list is one of the owner-only raw reads below.

  • Owner-only raw reads. A few older reads return a personal chat's stored messages without the sanitizer, so they still show the chat's owner an answer whose result is no longer available:

    • classic chat's /conversation/<id>/messages
    • the personal branch of /api/message/<id>/metadata
    • personal chat summaries: a new one is generated from the stored messages, and one already saved on the chat stays
    • MCP reads of personal chats

    Only the chat's owner can reach them, and saved-analysis answers have the same gap. Saved-analysis answers are also still copied into a collaboration as stored. Both are follow-ups shared with saved analysis, not fixed here.

Follow-ups

  • The workflow_results orchestration capability (step 9), in the next PR.
  • Structured (version 3) runs, with their node, execution and attempt selectors, plus hiding the buttons for converted workflows and structured alerts.
  • Named, non-authoritative outputs.
  • Output formats in Follow up.
  • Retry and Edit that keep the workflow result context. The server refuses them today.
  • Withholding unavailable answers on the owner-only raw reads (classic chat's message list, the personal message metadata route, personal summaries and MCP personal reads), and withholding saved-analysis answers in collaboration copies, together with saved analysis.
  • A retention setting (roadmap §9).
  • Group workflows (Phase 8).
  • Re-checking workflow result contexts when a generated file is published.
  • Phase 6b, which calls followUpWorkflowResult: delivery, the run card, the recurring-workflow card, the chat-list indicator and v2WorkflowRunPath.
  • Phase 6c's in-plan wait.

Adds the default-off admin setting enable_chat_workflow_results (classic and
V2 admin, update_settings real-boolean guard), the per-user gate
is_chat_workflow_results_enabled_for_user, the workflow_results_required route
decorator and the V2 bootstrap per-user override. Bumps VERSION to 0.261.212.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A Flask-free reader point-reads a personal workflow and run in the requester's partition, authorizes the whole run as run history does, binds the result to a canonical digest over its included outputs and the run status, and returns a public descriptor plus bounded, server-only excerpts. Closed reasons carry fixed wording and status codes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
GET /api/user/workflows/<workflow_id>/runs/<run_id>/result-context authorizes a finished personal run as its owner and returns only the public descriptor, uncached. Someone else's run answers exactly like a missing one, and every closed reason keeps its fixed status and wording. The shared reader fixtures move to test_support so the route, follow-up and masking tests can use them.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Follow up answers carry workflow_result metadata, and later answers that inherit the run carry workflow_result_contexts. normalize_history_message now drops both by key presence, so an orchestration plan never replays stored workflow output until the workflow_results capability can re-authorize it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds the Follow up executor for personal workflow runs (phase 6a, step 4).
A chat request that carries workflow_result_context {workflow_id, run_id,
result_sha256} is answered only from that run's stored result:

- functions_workflow_result_followup.py re-reads the run through the reader
  with the selected SHA on every turn (a changed result is a 409 and nothing
  is answered from the new one), refuses anything but the reader's own
  private personal chat, fences the excerpt as untrusted data, reuses the
  saved-analysis model call (selected model or local agent, tools off,
  context budget, screening on input and output) and appends a fixed,
  server-written disclosure.
- route_backend_chats.py dispatches it at the five analysis_result_context
  sites, before saved analysis and any source work. Both contexts on one
  request, retries and edits are refused up front, and the document-action
  and analyze streams refuse before creating a chat or a stream.
- The answer carries a public workflow_result descriptor and the accumulated
  workflow_result_contexts so later turns inherit the run.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Masking on read for Follow up answers (phase 6a, step 5). Every history,
export, collaboration and orchestration read already runs through
sanitize_saved_analysis_messages, so it now also recognizes messages that
carry workflow_result, workflow_result_context or workflow_result_contexts:

- A message is shown only in the reader's own private personal chat and only
  while every workflow result it relied on still authorizes for that reader
  with the same SHA. A deleted run, a changed result, lost source access, a
  converted or shared chat, or any storage failure withholds it; nothing is
  raised out of the read route.
- Each distinct result is authorized once and each chat is read once per
  call. A withheld answer keeps its id and role, loses its content,
  citations and thoughts, and carries {version, available: False}; the
  question keeps its text and loses its link to the result. Stored messages
  are untouched, so restoring access restores the answers.
- Conversation search skips its cache for workflow-result matches so a
  snippet can't outlive access.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mechanical: microsoft#1585 took 0.261.210, so the sequence is now 3c .211,
Phase 5 .212 and phase 6a .213. Only phase 6a's own version references
change.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in microsoft#1585 (0.261.210). The only conflict was VERSION in config.py,
kept at phase 6a's 0.261.213. The settings hunks in functions_settings.py
and admin_settings_fields.py merged cleanly. The regenerated docs
inventory is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 6a (microsoft#1546), step 6a. The composer can answer from one finished
personal workflow run's stored result.

- lib/workflowResults.ts: descriptor parsing (public fields only), the
  request shaper that turns every other source and the orchestration and
  analysis fields off, latest-turn inheritance, the server's fixed wording
  for every closed reason and which of them remove the chip, the
  descriptor read, and the chip sentence in the reader's locale and zone.
- lib/workflowResultFollowUp.ts: openWorkflowResultInChat and
  followUpWorkflowResult, the one entry point every surface (and 6b's
  delivered message, run card and recurring card) calls.
- lib/conversationUrl.ts: the one-shot result_workflow_id/result_run_id
  new-chat link, stripped by syncedConversationParams.
- chatStore: workflowResultContext beside analysisResultContext, mutually
  exclusive and sharing its revision; a launch that survives the first
  send and binds to the conversation it creates; inheritance from the
  latest answer; refusals shown in the fixed wording and clearing the
  chip; retry and edit refused before the server rebuilds the request
  without the run.
- Composer: the chip, sources and orchestration off while it is
  selected, sending held while Ask in chat is still reading the run.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 6a (microsoft#1546), step 6b of the plan.

- WorkflowRunAskInChat: Ask in chat on a personal workflow's finished run
  (completed or completed_partial) in the run history, hidden unless the
  bootstrap carries enable_chat_workflow_results. One JSX line and an
  import in WorkflowRunHistory.
- workflowAlertFollowUpAction returns { label: 'Ask about this', run } for
  a personal alert that names a run which had finished when it was raised,
  while the setting is on, and null for group alerts, alerts without a
  run, failed or cancelled runs, or the setting off. The alert card reads
  the flag and opens the chat through openWorkflowResultInChat; N2's card
  is otherwise unchanged.
- functional_tests/test_workflow_results_clients.mjs: descriptor parsing,
  the request shaper, inheritance, the fixed wording, the one-shot link
  and its strip, the alert action, and the descriptor read, bundled from
  the real modules with esbuild.
- V2_WORKFLOW_ALERT_NOTICES.md documents the action.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Playwright suite (25 tests) that bundles the real V2 chat page, composer,
chat store, run history and workflow alert card against a fake server that
answers with the real reader's and Follow up's fixed wording, codes and
disclosure line. Covers Ask in chat on personal runs only, the chip, an
inherited follow-up turn, descriptor and stream refusals (including a result
changed by resume-failed), a question sent while Ask in chat is opening, the
alert card's Ask about this, the setting off, reopening inherited and masked
chats, the Orchestrate toggle and a user-authored workflow name.

Also disables Send while Ask in chat is still opening, matching the submit
guard, so the button state agrees with what a click does.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add the Phase 6a feature doc (CHAT_WORKFLOW_RESULTS_FOLLOW_UP.md) and the
"Ask about workflow results" user guide, with its nav entry, guides index row
and features.yml links. Add the "Workflow results in chat (V2 interface)"
section to the chat controls reference, the Ask about this action to the
notifications guide, admin troubleshooting rows, a cross-link from the
Phase 4 proposals doc, and the v0.261.213 release notes.

Part of microsoft#1546. Part of microsoft#1543.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The reader's point reads are scoped to the requester's /user_id partition,
which is the owner check, and it compares the stored user_id again. The
fake containers honour partitions like Cosmos does, so no test could reach
the second comparison, and mutation testing showed dropping either one
went unnoticed. A partition-blind read now models a future lookup by id
alone, and each comparison must still refuse to pair someone else's
workflow or run with the requester's own.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread application/single_app/functions_workflow_result_followup.py Fixed
Comment thread application/single_app/functions_saved_analysis.py Fixed
CodeQL (py/call/wrong-arguments) read FollowUpServices.serialize as a bound
method. A plain function used as a dataclass default is also a class
attribute, and a function read from the class through an instance is bound.
The generated __init__ stores the value on the instance, so the calls worked,
but the six function defaults (serialize, gate, is_private, read_result,
authorize_context and now) now default to None, and __post_init__ sets the
real helpers on the instance, like the module's other optional hooks. No
behavior change.

A new test fails if any field default is a function again, and checks that
each omitted helper resolves to the real one and that an injected helper is
kept.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Address the review of 88106df.

- B1: converting a chat to a collaboration stores every message that uses
  a workflow result in its withheld form, through
  withhold_workflow_result_message in the new leaf module
  functions_workflow_result_masking.py, which read-time masking now
  shares. The metadata payload and source mirroring withhold the same
  way. When any copied message uses a workflow result, the
  collaboration's summary and the hidden source chat's summary are both
  cleared, so syncing can't restore them.
- B2: single-message export (Word, email draft, PowerPoint) authorizes
  the message with authorize_saved_analysis_message_read, which covers
  saved analyses and workflow results.
- B3: Retry and Edit of a Follow up turn return 400
  workflow_result_retry_unsupported before any content check, attempt
  query or write.
- B4: asserts that wrapped side effects capture the result first.
- B5: tests for the search-cache skip (R10) and the reader's own
  per-manifest task check (R27).
- B6: Follow up fences the run output between markers that carry a code
  drawn for each request, and the system message names that code.
- FollowUpServices replaces a helper only when it is None.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
R30: turnAsksAboutWorkflowResult finds a workflow_result_context or
workflow_result on the message itself when the message carries no
thread info.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Collaboration copies are stored with Follow up answers withheld, and
  the chat summary isn't carried over; the original chat's stored
  messages are unchanged.
- The owner-only raw reads this feature shares with saved analysis are
  listed as a limitation and a follow-up.
- The per-request fence code, single-message export checks, and the
  Retry and Edit refusal.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread functional_tests/test_support/workflow_result_offline_app.py Fixed
Paul Lizer (paullizer) and others added 7 commits October 1, 2026 03:27
CodeQL flagged the module-level `from uuid import uuid4`
(py/import-of-mutable-attribute) after the harness moved out of
test_workflow_result_chat_routes.py, where the import was function-level.
The harness now reads uuid.uuid4 at call time. Test-only, no behavior change.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The group route falls back to the personal store for a private chat that
only group context classifies. Follow up runs in that chat, and the
fallback already stored the copied answers withheld, but it copied the
summary raw and never cleared the hidden source chat's summary, so group
participants could read a summary of the withheld answers.

Both conversions now share _carry_summary_into_collaboration. When any
copied message uses a workflow result, the collaboration and the source
both drop the summary (key present, value None), after the M365
publication has fingerprinted the original. The group-container branch
can't hold Follow up messages, so its summary copies as before.

The review-paths scenario drives the from-group route: the copies and the
preview are withheld, both summaries are None, an ensure-then-sync round
trip with a regenerated source summary doesn't restore it, and a group
chat without workflow results keeps its summary on both chats.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
"The chat summary isn't carried over" undersold it: conversion also
clears the original chat's saved summary. The feature doc, release
notes, guide, chat controls and admin page now say "the chat's saved
summary is cleared on both chats".

The feature doc's collaboration section now describes the group route's
personal fallback for a chat that only group context classifies, and the
helper both conversions share, instead of saying group conversions don't
need this.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in the workflow_run capability (0.261.212). Conflicts:
- config.py keeps VERSION 0.261.213; a separate commit renumbers.
- release_notes.md keeps the 0.261.213 section above Phase 5's
  0.261.212 section, which is unchanged.
- chat-controls.md keeps the higher front-matter version and puts
  Phase 5's "Workflow runs (V2 interface)" section before
  "Workflow results in chat (V2 interface)".
The settings, admin fields, admin route, features.yml, docs and the
blueprint inventory test merged cleanly with both sides kept.
app_surface.yml is regenerated, not merged: both sides counted 123
capabilities, and the merged surface has 124.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mechanical: the 0.261.213 stamps become 0.261.214 in 6a's own
files: VERSION, the release-notes heading, the docs' version and
front-matter lines, the test headers' Version and Implemented in
lines, the assert_app_version_at_least floors and the Playwright
bootstrap seed. 0.261.213 now belongs to Phase 3c (microsoft#1593).

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 5 is on V2 now, so the feature doc no longer says the capability
waits for it to land. It ships in a later pull request built on this
reader and Phase 5's workflow planning context.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Brings in the V2 workflow editor's Ask AI tab (0.261.213). Conflicts:
- config.py keeps VERSION 0.261.214.
- docs/admin/workflow.md takes 3c's "Enable AI Workflow Assistant"
  row, with the Ask AI tab and Draft with AI wording, and keeps the
  "Use Workflow Results In Chat" row directly after it.
- release_notes.md keeps the 0.261.214 section above 3c's 0.261.213
  section. Every older section is unchanged.
features.yml and the rest of workflow.md merged cleanly.
app_surface.yml is unchanged: 3c added no settings, and the
regenerated inventory matches the merged file.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 1, 2026
Status update 4 for the chat orchestration workflows roadmap (docs only).

- Status table: 3c done (microsoft#1593, v0.261.213), so Phase 3 is complete;
  Phase 5 done (microsoft#1594, v0.261.212); 6a in review (microsoft#1592).
- 3c shipped block and its follow-ups; Phase 5 shipped block and its
  follow-ups; 6a in-review note; 6b run deep-link guidance now covers
  Phase 5's workflowRunHref.
- Base failures on paullizer-react-v2-ui that the phase PRs report,
  with verified causes.
- Gotchas 62 (result answers outside the message list) and 63 (Retry
  and Edit of a result-backed turn).

Refs microsoft#1543

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Saved analysis and Follow up asked the workflow planning context whether a
chat is private. The planning context's imports reach back to saved
analysis, so CodeQL reported py/cyclic-import on the lazy import in
functions_saved_analysis.py.

Move conversation_is_private, unchanged, into
functions_orchestration_memory.py next to validate_memory_audience, which
it wraps. The planning context imports it from there, so proposals, run
links and the planner keep the same function. Saved analysis and Follow up
import it lazily from the memory module, which imports none of them.

Add test_workflow_result_privacy_import_cycle.py: both lazy imports name
the memory module and never the planning context, the planning context
reuses the one definition, the memory module imports nothing from the
caller side, and each module loads cold in either order, with and without
-O, with no network access.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The feature doc's row for functions_orchestration_memory.py said saved
analysis and Follow up can check whether a chat is private "without an
import cycle". The move removes the cycle CodeQL reported, through the
workflow planning context. Wider loops made of function-level imports are
unchanged, so the row now says the function moved unchanged, that both
modules no longer import the planning context, and that the planning
context's imports closed the cycle CodeQL reported.

Part of microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) marked this pull request as ready for review October 1, 2026 16:25
@paullizer
Paul Lizer (paullizer) merged commit 520132d into microsoft:paullizer-react-v2-ui Oct 1, 2026
11 checks passed
Paul Lizer (paullizer) added a commit that referenced this pull request Oct 1, 2026
Brings in paullizer-react-v2-ui at 520132d (Phase 6a, VERSION 0.261.214).

Conflicts resolved by hand, without rerere:
- config.py: keep VERSION 0.261.215, one above 6a's 0.261.214.
- release_notes.md: the v0.261.215 section sits above 6a's v0.261.214
  section, which is unchanged byte for byte.

route_backend_workflows.py and docs/admin/workflow.md merged cleanly; the
diff against V2 is still only this PR's hunks.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 1, 2026
Brings in Phase 6a (workflow results in chat) so the harness fixes on this
branch are rerun against the route, collaboration, orchestration and V2 UI
changes it ships.

Refs microsoft#1543

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 1, 2026
microsoft#1592 (6a) moved the summary copy in both legacy conversions into
_carry_summary_into_collaboration, and made
_copy_legacy_personal_messages_to_collaboration withhold workflow-result
messages through functions_workflow_result_masking. The ConversionHarness
closed namespace bound none of these names, so every conversion raised
NameError (script: failures=8, errors=58; pytest: 66 failed).

Load _carry_summary_into_collaboration from production with the other
collaboration functions, and bind the real masking helpers, whose module
has no application imports. No production statements are copied. The
file is back to its pre-merge result: only the 4 known non_group_invitee
subtests fail, in all four modes.

The existing drift test names each unbound global, and removing any of
the three bindings fails it.

Refs microsoft#1543

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 1, 2026
Resolves the two conflicts with 0.261.214:
- config.py keeps VERSION 0.261.216.
- release_notes.md puts the v0.261.216 section above the new v0.261.214 section. Every V2 section is unchanged.

route_backend_workflows.py merged without conflicts. Its sanitize_log_message wraps and the extended functions_appinsights import are unchanged.

N9 (microsoft#1596, 0.261.215) has not landed on V2 yet. When it does, its release-notes section will need one more merge.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 1, 2026
Docs-only status update to the chat orchestration workflows roadmap at
paullizer-react-v2-ui 0.261.216:

- 6a done in microsoft#1592 (v0.261.214), with where it differs from the plan
- workflow_results capability and 6b (6b-1 server, 6b-2 V2) in progress;
  Retry uses the durable runtime/resume, not /resume-failed; run deep
  link reuses P's query form through one scope-aware helper
- N9 follow-up microsoft#1596 (v0.261.215) and microsoft#1597 log injection fix (v0.261.216)
- new follow-ups: microsoft#1598 in review, non_group_invitee product bug, stale
  base assertions, and the verified cold-import cycle
- section 10: branch from paullizer-react-v2-ui, not only base on it

Refs microsoft#1543 microsoft#1546

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit that referenced this pull request Oct 1, 2026
group_workflow_allowed_group_ids declares a two-link depends_on chain
(allow_group_workflows, then require_group_assignment_for_group_workflows),
so test_sub_settings_are_gated_by_their_capability crashed with
"'list' object has no attribute 'get'". Its docstring already recorded a
chain-aware fix, but that fix never landed in the code.

The check now reads every link through the application's own
iter_field_dependencies. The expected gate must be one of the links, with
equals True, and any other link must be one the gate itself requires. For
a single condition that is the original check. The ungated-limit message
path had the same single-dict assumption and is fixed too.
EXPECTED_DEPENDENCIES is unchanged; Phase 6a (#1592) edits it.

Refs #1543

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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.

2 participants