Repository navigation
Phase 6a: workflow results reader and Follow up in chat - #1592
Merged
Paul Lizer (paullizer) merged 27 commits intoOct 1, 2026
Conversation
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>
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>
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>
This was referenced Oct 1, 2026
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>
14 of 24 tasks
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>
This was referenced Oct 1, 2026
Paul Lizer (paullizer)
marked this pull request as ready for review
October 1, 2026 16:25
Paul Lizer (paullizer)
merged commit Oct 1, 2026
520132d
into
microsoft:paullizer-react-v2-ui
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>
14 of 24 tasks
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>
6 tasks
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.
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_resultsorchestration 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-importnote (decision 22). The version stays 0.261.214. The body wasn't updated for the47187e418round, so the next section covers it.11b04932fmovesconversation_is_privateintofunctions_orchestration_memory.py, unchanged, right aftervalidate_memory_audience, which it calls. A 2-line comment says why it lives there.OrchestrationMemoryErrorandvalidate_memory_audience. Its own definition is gone, and its two uses (L813 and L1067) are unchanged.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.f88e298f9merges V2 at70c255508(Roadmap status: Phase 3 complete, Phase 5 done, 6a in review #1595, which changes only the roadmap doc), with rerere off and no conflicts.badfff8aamakes the feature doc's memory-module row name the import cycle the move removes.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 base677c8f3e6has the same added and removed lines as its diff against4f778386b. The only other difference isapp_surface.yml's capability count, which moved with the base.f8763a09cmerges V2 at5fc50a593(Phase 5).config.py,release_notes.mdanddocs/reference/chat-controls.md.docs/_data/app_surface.ymlwas regenerated: 123 capabilities on each side, 124 merged.30b3fb08brenumbers .213 → .214, mechanically and before the 3c merge. It changesconfig.py, the release-notes heading, the docs' version and front-matter lines, the test headers, theassert_app_version_at_leastfloors and the Playwright bootstrap seed.477d1985fupdates 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.47187e418merges V2 at677c8f3e6(Phase 3c).config.py,docs/admin/workflow.mdandrelease_notes.md.app_surface.ymldidn't change. The merged tree,b7d1880be, matches the coordinator's trial merge.47187e418against677c8f3e6, 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.
7763cb130).validate_memory_audiencecounts a personal-store chat as personal when it'sgroup-single-useror 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_collaborationnow decides the summary for both conversions, afterprepare_m365_history_publication(decision 19).test_workflow_result_review_paths.pycover 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.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).
FollowUpServices.__post_init__checks each helper withis None, so a falsy injected fake is kept.withhold_workflow_result_messagelives in the new leaf modulefunctions_workflow_result_masking.py(decision 20). Read-time masking and collaboration storage both call it.message_kind,reply_to_message_idandsender, and the metadata'ssender,source_message_idandsource_role, so the copy still builds a valid collaboration doc.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).authorize_saved_analysis_message_read, with loaders that reuse the message and the chat it already read.workflow_result_retry_unsupported, before content screening or any write (decisions 9 and 17).python -Ocan't drop a call.secrets.token_hex(8), and only markers carrying it delimit the result.test_support/workflow_result_offline_app.py, subprocess output is decoded as UTF-8, and the harness importsuuidas 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)
The reader (
functions_workflow_result_reader.py, Flask-free)/user_idpartition. Then it checks the documents' owner andworkflow_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.completedandcompleted_partialare readable. Other runs get a closed reason:workflow_result_not_finishedworkflow_result_in_progressworkflow_result_preview_onlyworkflow_result_unsupported(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.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 acompleted_partialrun.[Excerpt truncated: the first N KB of M KB.]inside the fence.load_workflow_task_input(bounded=True)asSavedAnalysisInputs, 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.version: 'workflow-result-v1',workflow_id,run_id,workflow_name,status,completed_at,result_sha256andavailable. 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:Cache-Control: no-store, privateFollow up (
functions_workflow_result_followup.py)route_backend_chats.pygets a nested wrapper and thin dispatch at the fiveanalysis_result_contextsites.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_privateruns on every turn, and again before the answer is saved. It's Phase 4's rule, now kept infunctions_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.
workflow_result_changed, and nothing is answered from a different result.Untrusted data (gotcha 41). The server builds every model message itself, in this order:
<<<WORKFLOW RESULT <code> (untrusted data)>>>and<<<END WORKFLOW RESULT <code>>>>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_resultdescriptor and the accumulatedworkflow_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_messagesnow also recognizesworkflow_result,workflow_result_contextandworkflow_result_contexts, so every read site that already withholds saved analyses is covered at once.available: false. A question keeps its text and loses only its workflow keys. Both forms come fromwithhold_workflow_result_messagein the new leaf modulefunctions_workflow_result_masking.py, which has no application imports._load_export_message_for_user, which now callsauthorize_saved_analysis_message_readand so checks both lineages. Whole-conversation export already ran the sanitizer._copy_legacy_personal_messages_to_collaborationwithholds every message that uses a workflow result before building the copy, solast_message_previewis built from the withheld form. This includes the M365 publication's messages.Nonewith the key present, and the source chat's summary is cleared in the final source write.build_collaboration_message_metadata_payloadandmirror_source_message_to_collaborationwithhold a source message first.Orchestration history
normalize_history_messageskips Follow up answers and answers that inherited their context, until step 9 lets orchestration re-check a result itself.The 6b seam
lib/workflowResultFollowUp.tsexportsfollowUpWorkflowResult(descriptor, navigation)andopenWorkflowResultInChat(workflowId, runId, navigation).Nothing changes with the setting off
workflow_results_disabled.features.enable_chat_workflow_resultsis false, so V2 shows no buttons and no chip.Docs
docs/explanation/features/CHAT_WORKFLOW_RESULTS_FOLLOW_UP.mddocs/guides/ask-about-workflow-results.mddocs/admin/workflow.mdanddocs/reference/chat-controls.mdCHAT_ORCHESTRATION_WORKFLOW_PROPOSALS.md,V2_WORKFLOW_ALERT_NOTICES.md,manage-notifications.mdandtrigger-a-workflow.mddocs/_data/app_surface.ymlNot in this PR
workflow_resultsplanner 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).v2WorkflowRunPath.Files
Line counts are against V2 at
70c255508, over 65 files. Apart from decision 22's entries, they haven't changed sinceabc7f6a14.New
functions_workflow_result_reader.pyandfunctions_workflow_result_followup.pyfunctions_workflow_result_masking.py(68 lines): the leaf module with the message keys,message_uses_workflow_resultandwithhold_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.lib/workflowResults.ts: parsing, the request helper,latestWorkflowResultand the wordinglib/workflowResultFollowUp.ts: the 6b seamcomponents/chat/WorkflowResultChip.tsxcomponents/workflows/WorkflowRunAskInChat.tsxtest_support/workflow_result_offline_app.pyis the shared offline boot of the real app, used by the chat-routes and review-paths suites.test_workflow_result_privacy_import_cycle.pypins decision 22, with cold-import probes that reuse the existingtest_support/offline_bootstrap.py.Changed, server
route_backend_chats.py(+115 −4): the nested wrapper, dispatch and prechecks at the five sites, and inherited contexts in the history helpersfunctions_saved_analysis.py(+111 −2): masking on read, taking the withheld form from the leaf module, and the privacy check from the memory modulefunctions_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_collaborationroute_backend_conversation_export.py(+6 −4): single-message export checks both result lineagesroute_backend_conversations.py(+20): the search cache, and the Retry and Edit refusalsroute_backend_workflows.py(+30): the descriptor routefunctions_settings.py(+31): the default, theupdate_settingsguard, the gateis_chat_workflow_results_enabled_for_userand the decoratorworkflow_results_requiredadmin_settings_fields.py(+14),route_frontend_admin_settings.py(+3) andtemplates/admin/_panes/workflow.html(+11): the V2 and classic admin switchesroute_backend_v2.py(+5): the per-user bootstrap flagfunctions_orchestration_context.py(+4): the history exclusionfunctions_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 versionChanged, V2
stores/chatStore.ts(+174 −2)components/chat/Composer.tsx(+31 −8)pages/ChatPage.tsx(+30 −3)lib/conversationUrl.ts(+56 −1): the one-shotresult_workflow_idandresult_run_idparameters, read on the first render and stripped bysyncedConversationParamslib/types.ts(+20)lib/workflowAlertNotices.ts(+27 −5):workflowAlertFollowUpActioncomponents/notifications/WorkflowAlertCard.tsx(+8 −1): passes{ enabled, open }to itcomponents/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:
config.py,docs/_data/features.yml,docs/admin/workflow.mdanddocs/explanation/release_notes.mdadmin_settings_fields.py,functions_orchestration_workflow_context.py,functions_settings.pyandroute_frontend_admin_settings.pydocs/_data/app_surface.yml,CHAT_ORCHESTRATION_WORKFLOW_PROPOSALS.md,trigger-a-workflow.mdanddocs/reference/chat-controls.mdroute_tests/test_route_blueprint_policy_inventory.pyPhase 5's
enable_chat_orchestration_workflow_runsand this PR'senable_chat_workflow_resultssit in separate hunks, and both are kept. Only 4 files conflicted:config.py,release_notes.md,chat-controls.mdanddocs/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.pyorfunctions_orchestration_memory.py.The rest of Phase 5's original file list is still untouched here:
RunContext, the adapters androute_backend_orchestration.pyMessageList.tsx,OrchestrationPlanCard.tsx,orchestrationController.tsandworkflowRunLink.tsDecisions made autonomously
From the approved plan
enable_chat_workflow_results, in the Workflow tab, off by default, as the roadmap recommends.allow_user_workflowsand followsrequire_member_of_workflow_user. It doesn't needenable_chat_orchestration.completedandcompleted_partial.workflow_result_unsupported, and named outputs come later.(task_id, authoritative_output, result_ref.sha256)for every included task. It's taken from the same rows that authorization checks._report_numbers_supported), so text excerpts can't ride along in the same call.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.workflow_result_contexttogether with a retry or edit id gets the same refusal.time_zone, so the disclosure shows the user's local time. An invalid zone falls back to UTC.Made while building
workflow_result_unsupported, and the chip says so.WorkflowsSection.tsxandWorkflowRunHistory.tsx.WorkflowRunHistory.tsxwas 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 changedWorkflowsSection.tsx.Made while addressing the review
message_asks_about_workflow_result(workflow_resultorworkflow_result_context) instead ofmessage_uses_workflow_result, which also matchesworkflow_result_contexts._analysis_history_metadata()is spread only onto assistant and augmentation messages, so a question never carriesworkflow_result_contexts.turnAsksAboutWorkflowResultuses the same narrow rule.build_collaboration_message_metadata_payloadmerges the source message's metadata into the metadata route's payload, andmirror_source_message_to_collaborationcopies 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.summary: Nonewith the key present.ensure_collaboration_source_conversationpushes the value only when the key exists, so it pushesNoneonto the source, and a summary regenerated on the hidden source chat is cleared beforesync_collaboration_conversation_metadata_from_sourcecopies 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.functions_workflow_result_masking.pyhas no application imports. The reader, read-time masking, collaboration storage, chat routes and the conversation routes import the keys, the predicates andwithhold_workflow_result_messagefrom it, so collaboration storage doesn't import the reader. Compared with88106dfa5, every touched module has the same number of import edges that lead into a cycle, and the leaf is in no cycle.None, so nothing is written. The test pins 1 read (a miss) and 0 writes.Made at the user's request
functions_orchestration_memory.py, which fixes CodeQL'spy/cyclic-importnote (code-scanning alert 3919).validate_memory_audienceand catchesOrchestrationMemoryError, both defined there.Errors
The "Chip" column says what V2 does with the selected run when the refusal arrives.
workflow_results_disabledworkflow_result_invalid_contextworkflow_result_private_onlyworkflow_result_not_foundworkflow_result_access_deniedworkflow_result_changedworkflow_result_in_progressworkflow_result_not_finishedworkflow_result_preview_onlyworkflow_result_unsupportedworkflow_result_invalidworkflow_result_conversation_unavailableworkflow_result_context_conflictworkflow_result_retry_unsupportedworkflow_result_too_largeworkflow_result_model_unsupportedworkflow_result_answer_rejectedworkflow_result_storage_unavailableworkflow_result_answer_failedSign-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:
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 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
[WorkflowResults] Workflow result unavailable,[WorkflowResults] Follow up answer refusedand[WorkflowResults] A chat answer's workflow result was withheld on read.extracarries only the code, stage, check and exception type name._REASONSand never echo exception text (py/stack-trace-exposure).[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.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.result_refand 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.88106dfa5, with no behavior change.py/call/wrong-argumentsatfunctions_workflow_result_followup.pyL404.FollowUpServiceshad six dataclass fields whose defaults were plain functions:serialize,gate,is_private,read_result,authorize_contextandnow.services.serialize(user_doc)looked like one argument too many.__init__stores the value on the instance.None, and__post_init__sets the real helpers on the instance, like the module's other optional hooks.88106dfa5fixed it.11b04932f(decision 22).py/cyclic-import(recommendation) atfunctions_saved_analysis.pyL486, the function-level import ofconversation_is_privatefrom the planning context in read-time masking. The planning context led back tofunctions_saved_analysisthroughfunctions_orchestration_registry→functions_orchestration_native_results→functions_native_analysis_results.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.97355bc1e.93af89bab, CodeQL raisedpy/import-of-mutable-attributeatfunctional_tests/test_support/workflow_result_offline_app.pyL24. The offline boot had moved into that module with a module-levelfrom uuid import uuid4.test_workflow_draft_save_parity.pyreassignsuuid.uuid4(L297 and L304), and a name imported directly doesn't see that.import uuidanduuid.uuid4().hex. It's test-only.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_querycan stall a fresh interpreter, so the pytest runs put a session-localsitecustomize.pyonPYTHONPATHthat makes it raiseOSError, andplatformfalls back. The shim isn't committed and changes no test.What ran where. Head is
badfff8aa.f88e298f9.badfff8aachanges only one row of the feature doc.badfff8aa, against V2 at70c255508.f88e298f9with a clean worktree of677c8f3e6. V2's70c255508changes 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.test_workflow_result_reader.pytest_workflow_result_followup.pytest_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)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)test_workflow_result_masking.pytest_workflow_result_privacy_import_cycle.py(decision 22)test_workflow_result_orchestration_lineage.pytest_chat_workflow_results_admin.pyroute_tests/test_workflow_result_context_policy.pyExisting suites. 40 more files pass in both modes.
test_workflow_result_contract.py10,test_workflow_result_store.py38 (and 93 subtests) andtest_workflow_result_routes.py26.test_orchestration_memory_context.py13,test_orchestration_v2_plan_backend.py61 (and 88 subtests),test_v2_assist_thread.py14,test_assist_thread_submission_id.py61 (and 62 subtests),test_v2_workflow_ask_ai.py7 andtest_v2_workflow_ask_ai_parity.py12.test_orchestration_workflow_*:planning_context112,propose_capability88,proposal_routes60,proposal_imports6,proposals_admin12,setting_off_golden9,proposal_done_when2,proposal_editor_round_trip5,deliverable26,week_preview14,run_adapter129,run_answer18,run_approval_floor39,run_capability79,run_effects10,run_link_routes63,run_planning_context80,runs_admin14,runs_off_golden5 andrun_imports13. Alsotest_orchestration_deliverables.py53.test_saved_analysis_*:artifact_access11,cleanup7,diagnostics32,record_pages9,routes13,service14 andstore_integration26.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, raisesAttributeErrorongroup_workflow_allowed_group_ids, whose V2depends_onis a list. Base fails it the same way.test_saved_analysis_bounded_reports.py: 6 failed, 8 passed, on a stale monkeypatch target infunctions_workflow_context.test_saved_analysis_model_metadata.py: 2 failed, onassert None == 12000.The last two are also in the regression set, where base fails them identically.
Decision 22.
test_workflow_result_privacy_import_cycle.pyhas 11 tests:_WorkflowResultLineage._privateand_default_is_private, only from the memory module, and import nothing from the planning contextpython -Bprocesses, normally and under-O: the memory module alone, the callers first, and the planning context firstcollaboration_conversation_idand someone else's chat aren't.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.
677c8f3e647187e418functions_saved_analysis.pyfunctions_workflow_result_followup.pyfunctions_orchestration_workflow_context.pyfunctions_orchestration_memory.pyfunctions_workflow_result_reader.pyfunctions_workflow_result_masking.pyfunctions_orchestration_workflow_proposals.pyfunctions_orchestration_workflow_run_links.py47187e418, saved analysis and Follow up have each swapped their planning-context edge for a memory-module edge. Nothing else changed.functions_fact_memory_contextimport (L129) leads back to both callers. CodeQL reports no alert for them, but the modules aren't cycle-free.The six mutations ran on
11b04932fagainst the file's 11 tests, and all are killed: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
f88e298f9and on677c8f3e6, normally and underpython -O, then compared the failing test ids and their causes.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), andtest_collaboration_shared_conversation_management_fix.py,test_collaboration_metadata_and_mentions_fix.pyandtest_m365_collaboration_action_cards.py(1 each).test_group_collaboration_source_storage_fix.pyM365PolicyError: History sharing authorization is not configured(59) andNameError: M365ApprovalRequired(24)test_conversations_read_ownership_authorization.pyImportError: the test's fakeconfighas nocosmos_msgraph_pending_actions_containertest_per_message_powerpoint_export.pyNameError: resolve_block_sources_in_contenttest_collaboration_conversation_summary_export_fix.pytest_enhanced_citations_blob_and_collaboration_fix.pytest_collaboration_legacy_message_conversion.pyassert 'image' == 'user'test_scope_lock_collaboration_unlock_fix.pytest_persistent_conversation_summary.pyTypeError: Unrecognized credential typeOn base under
-O,test_m365_conversation_lifecycle.py'stest_real_conversation_deletion_fork_copy_and_retention_routesfailed once, in a 152 s run. A rerun passed all 5 in 25 s. Head passed all 5 under-Oin 23 s.Regression set. I ran 43 more files normally on
f88e298f9and on677c8f3e6, and compared failing test ids. They're 13 chat files, 9 saved-analysis, 9 route-policy and 12 from Phase 5, includingtest_orchestration_dependency_allowlist.py(41) andtest_v2_orchestration_workflow_run_links_xss_guardrail.py(4).test_workflow_result_context_requires_a_signed_in_user_sessionin the unauthenticated contract (5 tests against 4).test_analyze_backend_saved_integration.pyNameErrors (check_chat_content,checked_history_messages), so 503 instead of 200, 400 or 403test_v2_prompt_attachment_persistence.pyNameErrors (attach_chat_check,DEPENDENCY_PLAN_CONTRACT_VERSION,prepared_chat_image_references), so 500test_saved_analysis_bounded_reports.pytest_chat_stream_history_context_fix.pyNameError: checked_history_messagestest_chat_stream_retry_context_init_fix.pydocs/explanation/fixes, and a source-pattern assert aboutis_retrytest_saved_analysis_model_metadata.pyassert None == 12000test_v2_model_identity_and_scope.pyselection_keytest_route_backend_chats_redundant_assignment.pytest_chat_route_unreachable_code_cleanup.pytest_backend_chats_swagger_integration.pyTypeError: Unrecognized credential typeOther checks
.mjsfiles underfunctional_testsran, one of them thetest_support/tsResolve.mjshelper, and 57 exit 0.test_workflow_results_clients.mjspasses its 10 groups.testRetryGuardincludes R30's case, a message withworkflow_result_contextand no thread info.test_saved_analysis_clients.mjspasses.test_v2_workspace_action_authoring_logic.mjsfails withAssertionError: m365_calendar_plugin.py, on base too.npm run build, which runstsc -bbeforevite build(2703 modules), passes before and after the mutations.ui_tests, 266 tests, all pass.test_chat_workflow_results.py25,test_v2_workflow_alert_notices.py28,test_v2_orchestration_workflow_run_links.py29,test_v2_orchestration_workflow_proposal_card.py22,test_v2_orchestration_plan_editor.py35 andtest_v2_orchestration_dependency_plans.py32test_v2_workflow_ask_ai.py32,test_v2_workflow_ask_ai_proposal.py3,test_v2_assist_thread.py10 andtest_v2_plan_editor_references.py11test_v2_orchestration_workflow_run_floor.py9,test_admin_orchestration_workflow_runs.py9,test_v2_orchestration_streaming_bubble.py3,test_v2_orchestration_generated_images.py3 andtest_v2_chat_image_uploads.py15build_docs_inventory.pyshows no diff.test_docs_app_surface_coverage.pypasses 7/7,test_docs_site_quality.py6/6 andtest_docs_latest_release_catalog_parity.py6/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.features.ymllinks don't resolve (base: 2 of 68):CONTENT_SCREENING_FRAMEWORKandCHAT_CONTENT_CHECKS.reference/chat-controls.mdare 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 atabc7f6a14. 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:
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.test_a_resume_that_rewrites_an_included_result_changes_the_digest_and_nothing_is_readtest_shared_collaborative_converted_and_hidden_chats_are_refused_before_the_result_is_read[shared0]authorize_workflow_run_readtest_the_items_query_is_the_exact_projection_and_the_real_authorizer_accepts_its_rowstest_each_owner_comparison_holds_if_a_read_ever_loses_its_partition_scope[workflows]test_each_owner_comparison_holds_if_a_read_ever_loses_its_partition_scope[runs]workflow_idmatchtest_someone_elses_or_a_missing_run_reads_exactly_like_not_found[run_of_another_workflow]test_a_readable_result_in_the_owners_private_chat_is_shown_unchangedtest_one_long_text_output_uses_the_whole_budget_and_says_it_was_cutweb_search_enabledtest_workflow_results_clients.mjsc.workflow_idfrom the items projectiontest_a_finished_run_reads_as_its_owner_with_a_public_descriptortest_a_follow_up_answer_and_answers_that_inherited_it_stay_out_of_historyFollowUpServicestest_omitted_helpers_default_to_the_real_ones_without_becoming_methods__post_init__, so the helpers stayNonetest_a_question_is_answered_from_the_selected_run_only_with_the_disclosure, and 54 moretest_the_reader_rechecks_that_each_manifest_names_its_own_taskturnAsksAboutWorkflowResulttest_workflow_results_clients.mjs: "found on the message itself"_carry_summary_into_collaboration_carry_summary_into_collaborationtest_a_question_is_answered_from_the_selected_run_only_with_the_disclosuretest_the_fence_code_is_drawn_again_for_every_request_and_matches_its_system_messageM4a and M4b first survived. The point reads are partition-scoped, so the fake store never returned another user's document. Commit
8a94e4f5dadds the partition-blind test (decision 16), and both are now killed.Guardrails on
badfff8aa, against V2 at70c255508, over the 65 changed files. These mirror the CI workflows' file filters, and all pass..tsand.tsxfiles.pyfiles--full-fileover the full text of the 12 changed V2 filesabc7f6a14. 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 from0.261.213to0.261.214.example.test,model.invalidandsimplechat.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_resultscapability 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:
/conversation/<id>/messages/api/message/<id>/metadataOnly 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
workflow_resultsorchestration capability (step 9), in the next PR.followUpWorkflowResult: delivery, the run card, the recurring-workflow card, the chat-list indicator andv2WorkflowRunPath.