Skip to content

Phase 7a: hand off large chat requests to a one-time workflow (server, 0.261.238) - #1640

Merged
Paul Lizer (paullizer) merged 33 commits into
paullizer-react-v2-uifrom
paullizer-phase-7a-workflow-handoff
Oct 6, 2026
Merged

Paul Lizer (paullizer) merged 33 commits into
paullizer-react-v2-uifrom
paullizer-phase-7a-workflow-handoff

Conversation

@paullizer

Copy link
Copy Markdown
Contributor

Summary

Phase 7a, the server half of one-time workflow hand-off (#1549, part of #1543). When a chat request is too big for one plan (more documents, steps or time than a plan allows), the planner can now hand it to a one-time durable workflow instead of failing on its budget or giving a partial answer.

  • New capability workflow_handoff ("Hand off large work"): Reason role, manual approval floor, at most one per plan. Its step dry-runs a static blueprint (a loop over named documents or a workspace query, one review task and one report task) into a definition_version 3 For each workflow. It keeps a server-only sidecar on the step record and returns a hand-off card. Nothing runs until the user approves the plan and then the card.
  • Four requester-only routes read a plan's hand-offs, accept one as proposed or edited, decline one, and read its editor draft. Accepting creates the workflow disabled and one-time (origin.one_time: true, written only by the server) and queues exactly one durable run. The workflow id and the run's request id both derive from the hand-off, so a retry, a second tab or a crash finds the same workflow and run. When the run ends, the workflow records how in one_time_status, and 6b-1's delivery posts the report back into the conversation.
  • Admin: two new settings on the Chat Orchestration pane. Hand Off Large Work From Chat (enable_chat_orchestration_workflow_handoff) is off by default. Hand-offs from chat per user per day (chat_orchestration_max_workflow_handoffs_per_day) takes 1–100 and defaults to 5. Hand-off also needs Chat Orchestration, personal workflows, Propose Workflows From Chat, Run Workflows From Chat and Use Workflow Results In Chat, and works only in the user's own private conversations. With the setting off, or on with any other gate closed, planning and the Phase 4 routes are byte-identical to V2; the off-golden checks this.
  • Server only. Nothing under application/v2_ui/ changes. The V2 hand-off card is 7b, a separate PR. Until it ships, users can't accept a hand-off in the browser, so the setting should stay off.

Full design, contract and limits: docs/explanation/features/CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md.

Linked issue

Refs #1549 (Phase 7a; 7b, the V2 card, follows in a separate PR). Part of #1543.

Release Notes & Latest Features

  • New Feature
  • Bug Fix
  • UI Enhancement
  • Breaking Change
  • Internal only

Is this visible to end users?

  • Yes. Once an admin turns it on, hand-off plan steps and the server's hand-off notes appear in chat. Accepting a hand-off needs the 7b card.
  • No

Is this admin-facing (Admin Settings, governance, deployment, config)?

  • Yes. It adds two settings on the Chat Orchestration pane.
  • No

Should this become a Latest Feature card?

  • Yes
  • No. Not until 7b ships the card.
  • Already added

Screenshot needed for the card?

  • Yes
  • No
  • Attached

Version bump

Testing / validation

Still running. The one-process run and the standalone python and python -O runs of the files not yet in the 4-mode table are still going. I'll update this section when they finish.

Head d2af803. Base: unmodified V2 f1aeef1 (0.261.233). This branch has merged it and is 0 commits behind.

Everything ran on Windows with Python 3.12, from the repo root, with PYTHONPATH=application/single_app;functional_tests. A local sitecustomize.py was also on the path. It only stops Python's platform module from querying WMI, which hangs on this host, and it touches neither the app nor the tests. Each mode below is one of these commands:

Mode Command
pytest python -m pytest -p no:cacheprovider -q --no-header -rfE --tb=line functional_tests/<file> [more files]
pytest -O python -O -m pytest -p no:cacheprovider -q --no-header -rfE --tb=line functional_tests/<file> [more files]
python python functional_tests/<file>, the file's own __main__ runner
python -O python -O functional_tests/<file>

4-mode table

Suite pytest pytest -O python python -O
builder+routes 93 passed (rc 0) 93 passed (rc 0) – –
routes+builder 93 passed (rc 0) 93 passed (rc 0) – –
harness8 285 passed (rc 0) 285 passed (rc 0) – –
hand-off 11 + touched 9 763 passed (rc 0) 763 passed (rc 0) – –
test_orchestration_workflow_handoff_adapter.py – – ok (rc 0) ok (rc 0)
test_orchestration_workflow_handoff_capability.py – – 90 passed (rc 0) 90 passed (rc 0)
test_orchestration_workflow_handoff_imports.py – – 31 passed (rc 0) 31 passed (rc 0)
test_orchestration_workflow_handoff_off_golden.py – – 10 passed (rc 0) 10 passed (rc 0)
test_orchestration_workflow_handoff_planner.py – – 52 passed (rc 0) 52 passed (rc 0)
test_orchestration_workflow_handoff_routes.py – – 61 passed (rc 0) 61 passed (rc 0)
test_workflow_handoff_builder.py – – 32 passed (rc 0) 32 passed (rc 0)
test_workflow_handoff_end_to_end.py – – 3 passed (rc 0) 3 passed (rc 0)
test_workflow_handoff_lifecycle.py – – 26 passed (rc 0) 26 passed (rc 0)
test_workflow_handoff_origin.py – – 34 passed (rc 0) 34 passed (rc 0)
test_workflow_handoff_result_reader.py – – 69 passed (rc 0) 69 passed (rc 0)
route_tests/test_route_blueprint_policy_inventory.py – – ok (rc 0) ok (rc 0)
route_tests/test_route_unauthenticated_policy_contract.py – – ok (rc 0) –

pytest rows run their files in one process. builder+routes and routes+builder run the builder and routes tests together in both orders. harness8 runs the 8 files that share the draft save-parity harness. hand-off 11 + touched 9 runs the 11 new hand-off test files with the 8 existing test files this PR edits and the route-policy coverage test. File rows run each file standalone. ok means the file's own runner passed without printing a pytest summary.

One process, everything together

Still running. The only failures expected are the 4 base failures explained in the next paragraph.

The 4 failures are the base's test_orchestration_workflow_proposal_editor_round_trip.py failures. That test drives the V2 TypeScript editor through Node and asserts that application/v2_ui/node_modules exists, which isn't installed in these checkouts. On f1aeef1 the same 4 tests fail under pytest, pytest -O, python and python -O.

Extra reruns: A5, A13, Phase 4 and Phase 5

Suite pytest pytest -O python python -O
test_workflow_loop_reporting.py 35 passed (rc 0) 35 passed (rc 0) ok (rc 0) ok (rc 0)
test_orchestration_actions_admin.py 8 passed (rc 0) 8 passed (rc 0) ok (rc 0) ok (rc 0)
test_orchestration_workflow_proposal_done_when.py – – 2 passed (rc 0) 2 passed (rc 0)
test_orchestration_workflow_proposal_editor_round_trip.py – – 1 passed, 4 failed (rc 1) 1 passed, 4 failed (rc 1)
test_orchestration_workflow_proposal_imports.py – – 6 passed (rc 0) 6 passed (rc 0)
test_orchestration_workflow_proposal_routes.py – – 60 passed (rc 0) 60 passed (rc 0)
test_orchestration_workflow_proposals_admin.py – – 12 passed (rc 0) 12 passed (rc 0)
test_orchestration_workflow_propose_capability.py – – 88 passed (rc 0) 88 passed (rc 0)
test_orchestration_workflow_run_adapter.py – – 137 passed (rc 0) 137 passed (rc 0)
test_orchestration_workflow_run_answer.py – – 18 passed (rc 0) 18 passed (rc 0)
test_orchestration_workflow_run_approval_floor.py – – 39 passed (rc 0) 39 passed (rc 0)
test_orchestration_workflow_run_capability.py – – 79 passed (rc 0) 79 passed (rc 0)
test_orchestration_workflow_run_effects.py – – 10 passed (rc 0) 10 passed (rc 0)
test_orchestration_workflow_run_imports.py – – 13 passed (rc 0) 13 passed (rc 0)
test_orchestration_workflow_run_link_routes.py – – 63 passed (rc 0) 63 passed (rc 0)
test_orchestration_workflow_run_planning_context.py – – 80 passed (rc 0) 80 passed (rc 0)
test_orchestration_workflow_runs_admin.py – – 14 passed (rc 0) 14 passed (rc 0)
test_orchestration_workflow_runs_off_golden.py – – 5 passed (rc 0) 5 passed (rc 0)
test_v2_orchestration_workflow_run_links_xss_guardrail.py – – ok (rc 0) ok (rc 0)
test_workflow_loop_schema.py – – ok (rc 0) ok (rc 0)

test_workflow_loop_reporting.py (A13) and test_orchestration_actions_admin.py (A5's admin pin) run in all four modes. The 18 Phase 4 and Phase 5 files run standalone here; their pytest runs are in the one-process run above and the base comparison below. test_orchestration_workflow_proposal_editor_round_trip.py's 4 failures are the base failures described above.

Base comparison, file by file

Each file ran in its own pytest process on the head and on f1aeef1. The head has no new failures: every head failure also fails on the base, with the same test id.

File HEAD Base f1aeef1 HEAD failures not on base
test_orchestration_workflow_proposal_editor_round_trip.py 1 passed, 4 failed (rc 1) 1 passed, 4 failed (rc 1) none
test_orchestration_plan_revision_references.py 24 failed (rc 1) 24 failed (rc 1) none
test_orchestration_plan_revision_routes.py 29 failed (rc 1) 29 failed (rc 1) none
test_workflow_runtime_integration.py 10 passed, 4 failed (rc 1) 10 passed, 4 failed (rc 1) none
test_admin_settings_group_shared_regions.py 4 passed, 1 failed (rc 1) 4 passed, 1 failed (rc 1) none
test_admin_settings_nav_map.py 3 passed, 1 failed (rc 1) 3 passed, 1 failed (rc 1) none
test_admin_settings_safe_int_fallback_fix.py 2 passed, 1 failed (rc 1) 2 passed, 1 failed (rc 1) none
test_admin_settings_sidebar_card_parity.py 4 passed, 1 failed (rc 1) 4 passed, 1 failed (rc 1) none
test_admin_settings_tab_preservation.py 2 passed, 1 failed (rc 1) 2 passed, 1 failed (rc 1) none
test_app_settings_cache_versioning.py 1 passed, 1 failed (rc 1) 1 passed, 1 failed (rc 1) none
74 files pass on both sides
  • test_m365_run_as_self_authored.py: 51 passed (rc 0) at the head; 51 passed (rc 0) on the base
  • test_workflow_assist_dry_run_parity.py: 16 passed (rc 0) at the head; 16 passed (rc 0) on the base
  • test_workflow_draft_save_parity.py: 26 passed (rc 0) at the head; 26 passed (rc 0) on the base
  • test_workflow_draft_service.py: 40 passed (rc 0) at the head; 38 passed (rc 0) on the base
  • test_workflow_draft_v2_round_trip.py: 19 passed (rc 0) at the head; 19 passed (rc 0) on the base
  • test_workflow_handoff_builder.py: 32 passed (rc 0) at the head; absent on the base
  • test_workflow_handoff_origin.py: 34 passed (rc 0) at the head; absent on the base
  • test_workflow_origin_provenance.py: 67 passed (rc 0) at the head; 67 passed (rc 0) on the base
  • test_orchestration_workflow_proposal_done_when.py: 2 passed (rc 0) at the head; 2 passed (rc 0) on the base
  • test_orchestration_workflow_proposal_imports.py: 6 passed (rc 0) at the head; 6 passed (rc 0) on the base
  • test_orchestration_workflow_proposal_routes.py: 60 passed (rc 0) at the head; 60 passed (rc 0) on the base
  • test_orchestration_workflow_proposals_admin.py: 12 passed (rc 0) at the head; 12 passed (rc 0) on the base
  • test_orchestration_workflow_propose_capability.py: 88 passed (rc 0) at the head; 88 passed (rc 0) on the base
  • test_orchestration_workflow_run_adapter.py: 137 passed (rc 0) at the head; 129 passed (rc 0) on the base
  • test_orchestration_workflow_run_answer.py: 18 passed (rc 0) at the head; 18 passed (rc 0) on the base
  • test_orchestration_workflow_run_approval_floor.py: 39 passed (rc 0) at the head; 39 passed (rc 0) on the base
  • test_orchestration_workflow_run_capability.py: 79 passed (rc 0) at the head; 79 passed (rc 0) on the base
  • test_orchestration_workflow_run_effects.py: 10 passed (rc 0) at the head; 10 passed (rc 0) on the base
  • test_orchestration_workflow_run_imports.py: 13 passed (rc 0) at the head; 13 passed (rc 0) on the base
  • test_orchestration_workflow_run_link_routes.py: 63 passed (rc 0) at the head; 63 passed (rc 0) on the base
  • test_orchestration_workflow_run_planning_context.py: 80 passed (rc 0) at the head; 80 passed (rc 0) on the base
  • test_orchestration_workflow_runs_admin.py: 14 passed (rc 0) at the head; 14 passed (rc 0) on the base
  • test_orchestration_workflow_runs_off_golden.py: 5 passed (rc 0) at the head; 5 passed (rc 0) on the base
  • test_v2_orchestration_workflow_run_links_xss_guardrail.py: 4 passed (rc 0) at the head; 4 passed (rc 0) on the base
  • test_workflow_loop_schema.py: 212 passed (rc 0) at the head; 212 passed (rc 0) on the base
  • test_orchestration_workflow_results_aliases.py: 10 passed (rc 0) at the head; 10 passed (rc 0) on the base
  • test_orchestration_workflow_results_answer.py: 18 passed (rc 0) at the head; 18 passed (rc 0) on the base
  • test_orchestration_workflow_results_capability.py: 70 passed (rc 0) at the head; 70 passed (rc 0) on the base
  • test_orchestration_workflow_results_imports.py: 26 passed (rc 0) at the head; 26 passed (rc 0) on the base
  • test_orchestration_workflow_results_off_golden.py: 5 passed (rc 0) at the head; 5 passed (rc 0) on the base
  • test_orchestration_workflow_results_reads.py: 53 passed (rc 0) at the head; 53 passed (rc 0) on the base
  • test_orchestration_workflow_results_time_zone.py: 8 passed (rc 0) at the head; 8 passed (rc 0) on the base
  • test_orchestration_workflow_deliverable.py: 26 passed (rc 0) at the head; 26 passed (rc 0) on the base
  • test_workflow_chat_delivery_concurrency.py: 5 passed (rc 0) at the head; 5 passed (rc 0) on the base
  • test_workflow_chat_delivery_contract.py: 72 passed (rc 0) at the head; 72 passed (rc 0) on the base
  • test_workflow_chat_delivery_control_pins.py: 35 passed (rc 0) at the head; 35 passed (rc 0) on the base
  • test_workflow_chat_delivery_imports.py: 31 passed (rc 0) at the head; 31 passed (rc 0) on the base
  • test_workflow_chat_delivery_loop.py: 9 passed (rc 0) at the head; 9 passed (rc 0) on the base
  • test_workflow_chat_delivery_notice_and_unread.py: 25 passed (rc 0) at the head; 25 passed (rc 0) on the base
  • test_workflow_chat_delivery_off_golden.py: 6 passed (rc 0) at the head; 6 passed (rc 0) on the base
  • test_workflow_chat_delivery_placement_and_masking.py: 8 passed (rc 0) at the head; 8 passed (rc 0) on the base
  • test_workflow_chat_delivery_projection_and_seed.py: 16 passed (rc 0) at the head; 16 passed (rc 0) on the base
  • test_workflow_chat_delivery_refusals.py: 2 passed (rc 0) at the head; 2 passed (rc 0) on the base
  • test_workflow_chat_delivery_save_guard.py: 14 passed (rc 0) at the head; 14 passed (rc 0) on the base
  • test_workflow_chat_delivery_status_route.py: 48 passed (rc 0) at the head; 48 passed (rc 0) on the base
  • test_workflow_chat_delivery_worker.py: 112 passed (rc 0) at the head; 112 passed (rc 0) on the base
  • test_orchestration_plan_revision_planner.py: 11 passed (rc 0) at the head; 11 passed (rc 0) on the base
  • test_orchestration_plan_revision_store.py: 44 passed (rc 0) at the head; 44 passed (rc 0) on the base
  • test_orchestration_failure_telemetry.py: 18 passed (rc 0) at the head; 18 passed (rc 0) on the base
  • test_orchestration_actions_admin.py: 8 passed (rc 0) at the head; 8 passed (rc 0) on the base
  • test_admin_settings_absent_field_preservation.py: 3 passed (rc 0) at the head; 3 passed (rc 0) on the base
  • test_admin_settings_dependencies.py: 4 passed (rc 0) at the head; 4 passed (rc 0) on the base
  • test_admin_settings_diagnostics_routes.py: 4 passed (rc 0) at the head; 4 passed (rc 0) on the base
  • test_admin_settings_field_contract.py: 2 passed (rc 0) at the head; 2 passed (rc 0) on the base
  • test_admin_settings_fields_registry_integrity.py: 5 passed (rc 0) at the head; 5 passed (rc 0) on the base
  • test_admin_settings_modal_placement.py: 2 passed (rc 0) at the head; 2 passed (rc 0) on the base
  • test_admin_settings_pane_variable_scope.py: 5 passed (rc 0) at the head; 5 passed (rc 0) on the base
  • test_admin_settings_sidebar_and_agent_catalog_guard.py: 2 passed (rc 0) at the head; 2 passed (rc 0) on the base
  • test_admin_settings_template_composition.py: 5 passed (rc 0) at the head; 5 passed (rc 0) on the base
  • test_admin_settings_walkthrough_targets.py: 3 passed (rc 0) at the head; 3 passed (rc 0) on the base
  • route_tests/test_route_blueprint_policy_inventory.py: 12 passed (rc 0) at the head; 11 passed (rc 0) on the base
  • route_tests/test_route_policy_test_coverage.py: 2 passed (rc 0) at the head; 2 passed (rc 0) on the base
  • route_tests/test_route_unauthenticated_policy_contract.py: 7 passed (rc 0) at the head; 6 passed (rc 0) on the base
  • route_tests/test_workflow_alert_since_hours_policy.py: 18 passed (rc 0) at the head; 18 passed (rc 0) on the base
  • route_tests/test_workflow_assist_policy.py: 64 passed (rc 0) at the head; 64 passed (rc 0) on the base
  • route_tests/test_workflow_execution_journal_policy.py: 3 passed (rc 0) at the head; 3 passed (rc 0) on the base
  • route_tests/test_workflow_flow_inspection_policy.py: 22 passed (rc 0) at the head; 22 passed (rc 0) on the base
  • route_tests/test_workflow_loop_policy.py: 3 passed (rc 0) at the head; 3 passed (rc 0) on the base
  • route_tests/test_workflow_repeat_policy.py: 2 passed (rc 0) at the head; 2 passed (rc 0) on the base
  • route_tests/test_workflow_result_context_policy.py: 38 passed (rc 0) at the head; 38 passed (rc 0) on the base
  • test_orchestration_dependency_allowlist.py: 41 passed (rc 0) at the head; 41 passed (rc 0) on the base
  • test_workflow_orchestration_limits_settings.py: 28 passed (rc 0) at the head; 28 passed (rc 0) on the base
  • test_docs_app_surface_coverage.py: 7 passed (rc 0) at the head; 7 passed (rc 0) on the base
  • test_docs_site_quality.py: 6 passed (rc 0) at the head; 6 passed (rc 0) on the base

Absent on the base because this PR adds them: test_workflow_handoff_builder.py, test_workflow_handoff_origin.py.

Why these files fail on the base:

  • test_orchestration_plan_revision_references.py (24) and test_orchestration_plan_revision_routes.py (29): every test stops on ImportError: cannot import name 'enabled_required' from 'functions_settings'.

  • test_workflow_runtime_integration.py (4): the background run ends failed where the tests expect completed (3) or waiting_approval (1).

  • test_orchestration_workflow_proposal_editor_round_trip.py (4): it needs application/v2_ui/node_modules, which isn't installed in these checkouts.

  • 5 admin-settings files and test_app_settings_cache_versioning.py (1 failure each):

    • V2's composed Admin Settings template declares inbound-mcp-configuration and support_latest_features_settings twice.
    • It has a content-screening tab with no pane, and an m365-retrieval-configuration card with no sidebar destination.
    • One test looks for VERSION = "0.240.002". Another looks for a functions_settings.py call that V2 no longer has.
    • test_admin_settings_tab_preservation.py returns False, which the pytest reports functional tests that return False as passed #1572 guard fails.

    This PR touches admin_settings_fields.py, functions_settings.py, route_frontend_admin_settings.py and the Chat Orchestration pane template, so I re-ran these 6 failing tests on both SHAs. Their assertion messages are identical.

End to end (A6)

test_workflow_handoff_end_to_end.py accepts a hand-off over a workspace query of 3 documents and of 200, then runs it through the real durable runtime, workflow runner, For each, Collect, report paging, and 6b-1's delivery worker and result reader. Only Cosmos, the model, the per-document Analyze, document enumeration and the lease heartbeat are faked.

No fallback was needed: both legs run the full count in all four modes. The 200-item leg checks the A13 bounds on the real reporting path:

  • every report page holds at most MAX_REPORT_PAGE_RECORDS (100) records and ANALYSIS_RECORD_PAGE_BYTES (128 KiB);
  • the page count matches the output reserve;
  • no reduction has more than MAX_REDUCTION_CHILDREN children.

It also sweeps every response and app log, from accept through delivery, for private values and internal hand-off ids.

Off-golden (A1, A18)

test_orchestration_workflow_handoff_off_golden.py compares planning and the Phase 4 routes against a fixture written with --write-golden on unmodified V2. It covers the setting off, and the setting on with another gate off. The fixture was captured on fd842e1, then re-captured on ca95275 (#1630), f7e268c and f1aeef1 (08d28e2). Each re-capture differed only in its captured_from label; none was hand-edited. No other golden fixture changes in this diff, and every existing golden passes unchanged at the head and on f1aeef1. Phase 4's setting off-golden (9 passed), the reference-authorizer golden (2 tests, 74 subtests) and the run-prompt golden in test_workflow_run_time_context.py (13 passed) pass in all four modes on both sides. Per file under pytest: Phase 5's runs (5 passed (rc 0) at the head), 6a's results (5 passed (rc 0) at the head), 6b-1's chat delivery (6 passed (rc 0) at the head).

Mutation testing

The 84 mutants are plan §19's list, including the ones approval notes A7 and A11–A17 added. Each is applied alone to a production file under application/single_app/. Its target tests then run twice: once with python -m pytest <tests> -x -q -p no:cacheprovider --rootdir functional_tests -W ignore ("normal" in the table) and once with the same command under python -O ("-O"). Every kill below is a named test failing with exit code 1, never a collection error. 82 of 84 are killed in both modes. The 2 survivors, 50 and 51, are equivalent:

  • 50 (actor_user_id=None): every use in build_personal_workflow_document goes through modifying_user_id = actor_user_id or user_id, so the requester is still the actor and modified_by.
  • 51 (keep the payload's URL Access authorization fields): the build clears authorization unless URL Access is on, and _requests_url_access refuses URL Access before the build.

Four mutants didn't die on the first pass: 57b, 3b, 55 and 49b. 49b's first-pass kill was false, caused by the harness pollution that bacbf0a fixes. Each of the four now dies to a new test from f31d45f.

Kill table (84 mutants)
# Mutation Killed by normal -O
1 accept: a user other than the requester can act on a hand-off test_orchestration_workflow_handoff_routes::test_only_the_requester_can_read_or_decide_a_handoff killed killed
1b status: a user other than the requester can read a run's hand-offs test_orchestration_workflow_handoff_routes::test_only_the_requester_can_read_or_decide_a_handoff killed killed
2 a hand-off route acts on a Phase 4 proposal id as not found instead of the other kind test_orchestration_workflow_handoff_routes::test_phase_4_refuses_a_handoff_and_a_handoff_route_refuses_a_proposal killed killed
3 a Phase 4 route reports a hand-off id as not found instead of the other kind test_orchestration_workflow_handoff_routes::test_phase_4_refuses_a_handoff_and_a_handoff_route_refuses_a_proposal killed killed
3b ¹ a Phase 4 accept adopts a one-time hand-off workflow as its own test_orchestration_workflow_handoff_routes::test_a_one_time_workflow_at_a_proposals_id_is_never_taken_for_the_proposals_workflow killed killed
4a a blueprint hand-off is created enabled test_workflow_handoff_builder::test_named_documents_build_a_durable_version_3_for_each killed killed
4b an edited hand-off keeps the payload's is_enabled test_workflow_handoff_builder::test_an_edited_hand_off_stays_a_paused_manual_durable_workflow killed killed
4c the builder emits an enabled definition test_workflow_handoff_builder::test_the_builder_is_deterministic_and_bounded killed killed
5 the run request id is random, so a retry queues a second run test_orchestration_workflow_handoff_routes::test_accept_creates_the_workflow_disabled_and_queues_exactly_one_durable_run killed killed
6 accept confirms and queues a workflow id that is not the derived one test_orchestration_workflow_handoff_routes::test_accept_creates_the_workflow_disabled_and_queues_exactly_one_durable_run killed killed
7 accept creates without claiming the decision first test_orchestration_workflow_handoff_routes::test_a_create_runs_under_a_claim_that_is_released_when_it_fails killed killed
8 a failed create never releases its claim test_orchestration_workflow_handoff_routes::test_a_create_runs_under_a_claim_that_is_released_when_it_fails killed killed
9 the hand-off workflow id is random, so a second accept creates a second workflow test_workflow_handoff_builder::test_a_hand_off_is_created_once_paused_and_one_time killed killed
10 a retry never finds the run an earlier accept queued test_orchestration_workflow_handoff_routes::test_an_accept_whose_decisions_were_not_recorded_still_settles_on_one_run killed killed
11 hand-off workflows count toward Phase 4's per-user workflow cap test_orchestration_workflow_handoff_routes::test_a_handoff_does_not_count_toward_phase_4s_workflow_cap killed killed
12a the rolling daily hand-off limit is never checked test_orchestration_workflow_handoff_routes::test_the_daily_limit_counts_recent_handoffs_including_ones_being_deleted killed killed
12b the daily limit window is one hour instead of 24 test_orchestration_workflow_handoff_routes::test_the_decision_field_claim_window_and_request_ids_are_the_hand_offs_own killed killed
13 a hand-off's alert rules open a pop-up instead of the bell test_workflow_handoff_builder::test_alerts_are_notify_only_on_every_run_at_info_severity killed killed
14 hand-off alerts are warnings instead of info test_workflow_handoff_builder::test_named_documents_build_a_durable_version_3_for_each killed killed
15 an edited hand-off may keep a Microsoft 365 Run as test_workflow_handoff_builder::test_an_edited_hand_off_stays_a_paused_manual_durable_workflow killed killed
16 the builder sets a Microsoft 365 Run as test_workflow_handoff_builder::test_named_documents_build_a_durable_version_3_for_each killed killed
17a planner section emitted when unavailable (Plan limits) test_orchestration_workflow_handoff_planner::test_a_request_that_cannot_hand_off_sees_no_hand_off_guidance[absent-all_on] killed killed
17b planner section emitted when unavailable (hand-off instructions) test_orchestration_workflow_handoff_planner::test_a_request_that_cannot_hand_off_sees_no_hand_off_guidance[absent-all_on] killed killed
18 off-golden drift (one byte in the planner system prompt) test_orchestration_workflow_handoff_off_golden::test_planning_is_byte_identical_when_handoff_is_unavailable[absent] killed killed
19 the sidecar (model_selection, ids) leaks into the public step record test_orchestration_workflow_handoff_adapter::test_the_step_record_keeps_the_sidecar_and_the_public_record_drops_it killed killed
20a the sidecar leaks an id into a stream frame (through the step summary) test_orchestration_workflow_handoff_adapter::test_the_step_keeps_its_sidecar_on_the_server_and_retains_only_the_card killed killed
20b the sidecar leaks into a stream frame (both step-frame allowlists widened) test_orchestration_workflow_handoff_adapter::test_the_step_keeps_its_sidecar_on_the_server_and_retains_only_the_card killed killed
21 recording one_time_status also changes modified_at test_workflow_handoff_lifecycle::test_each_terminal_state_records_how_the_hand_off_ended[cancelled] killed killed
22 a failing one_time_status write breaks finalization test_workflow_handoff_lifecycle::test_a_failing_status_write_never_breaks_finalization killed killed
23 the hand-off run is queued with a manual trigger source test_orchestration_workflow_handoff_routes::test_accept_creates_the_workflow_disabled_and_queues_exactly_one_durable_run killed killed
24 the run's chat invocation does not carry the hand-off id test_orchestration_workflow_handoff_routes::test_accept_creates_the_workflow_disabled_and_queues_exactly_one_durable_run killed killed
25a the delivery record ignores the time zone the sidecar captured test_orchestration_workflow_handoff_routes::test_accept_creates_the_workflow_disabled_and_queues_exactly_one_durable_run killed killed
25b the delivery record ignores the model selection the sidecar captured test_orchestration_workflow_handoff_routes::test_accept_creates_the_workflow_disabled_and_queues_exactly_one_durable_run killed killed
26 the hand-off approval floor removed test_orchestration_workflow_handoff_capability::test_the_descriptor_is_a_reason_step_with_a_manual_floor_and_one_step_per_plan killed killed
27 max_per_plan removed from the hand-off descriptor test_orchestration_workflow_handoff_capability::test_the_descriptor_is_a_reason_step_with_a_manual_floor_and_one_step_per_plan killed killed
28 external_effects: True added to the hand-off descriptor test_orchestration_workflow_handoff_capability::test_the_descriptor_is_a_reason_step_with_a_manual_floor_and_one_step_per_plan killed killed
29a the hand-off step module imports Flask test_orchestration_workflow_handoff_adapter::test_the_module_imports_no_flask_store_or_runtime killed killed
29b the hand-off step touches Flask when it runs test_orchestration_workflow_handoff_adapter::test_the_module_imports_no_flask_store_or_runtime killed killed
30 a storage failure leaks the exception text instead of the closed code test_orchestration_workflow_handoff_routes::test_a_create_runs_under_a_claim_that_is_released_when_it_fails killed killed
31 result reader opens a structured run without the chat-orchestration trigger test_workflow_handoff_result_reader::test_every_other_structured_shape_stays_closed_with_no_read[trigger_manual] killed killed
32 result reader opens a structured run with more than one workflow output test_workflow_handoff_result_reader::test_two_outputs_or_another_handoff_stay_closed killed killed
33 result reader opens a structured workflow that isn't one-time test_workflow_handoff_result_reader::test_every_other_structured_shape_stays_closed_with_no_read[one_time_false] killed killed
34 origin normalization accepts a client one_time test_workflow_handoff_origin::test_a_chat_proposal_create_never_records_one_time[personal] killed killed
35 repairable budget code when hand-off is unavailable test_orchestration_workflow_handoff_planner::test_a_budget_overflow_without_hand_off_fails_as_before[absent-all_on] killed killed
36a repairable budget code missing when hand-off is available (schema) test_orchestration_workflow_handoff_planner::test_a_budget_overflow_is_repaired_once_when_hand_off_is_offered killed killed
36b repairable budget code missing when hand-off is available (planner) test_orchestration_workflow_handoff_planner::test_a_budget_overflow_is_repaired_once_when_hand_off_is_offered killed killed
37 scope handles emitted outside the hand-off catalog test_orchestration_workflow_handoff_planner::test_an_offered_hand_off_adds_instructions_limits_and_its_own_catalog[on-all_on] killed killed
38 silent_when_unavailable ignored test_orchestration_workflow_handoff_capability::test_a_request_that_cannot_hand_off_is_not_offered_it_and_gets_no_reason[on-orchestration_off] killed killed
39a D3 hand-off guidance (fact and recipe) emitted when hand-off is unavailable test_orchestration_workflow_handoff_off_golden::test_planning_is_byte_identical_when_handoff_is_unavailable[absent] killed killed
39b D3 hand-off guidance (fact and recipe) missing when hand-off is available test_orchestration_workflow_handoff_planner::test_an_offered_hand_off_adds_instructions_limits_and_its_own_catalog[on-all_on] killed killed
40 hand-off note emitted for non-hand-off plans test_orchestration_workflow_handoff_adapter::test_the_reply_notes_a_ready_hand_off_and_says_why_another_was_not_handed_off killed killed
41 a workflow_run step starts a one-time hand-off workflow again test_orchestration_workflow_run_adapter::test_a_one_time_handoff_workflow_is_never_started_again_from_chat killed killed
42 a daily count that cannot be read lets the accept through (fails open) test_orchestration_workflow_handoff_routes::test_a_daily_count_that_cannot_be_read_fails_closed killed killed
43a one_time enters the Microsoft 365 execution fingerprint test_workflow_handoff_origin::test_one_time_is_outside_the_revision_and_the_fingerprint killed killed
43b one_time enters the definition revision test_workflow_handoff_origin::test_one_time_is_outside_the_revision_and_the_fingerprint killed killed
43c a repeated create adopts a record of the other kind (one-time or not) test_workflow_handoff_origin::test_a_hand_off_and_a_chat_proposal_never_adopt_each_other[found] killed killed
43d the origin itself is a material field, so a re-save marks the workflow edited test_workflow_handoff_origin::test_an_editor_save_keeps_a_hand_off_one_time[2-lookalike] killed killed
44a an editor save drops the stored one_time_status test_workflow_handoff_lifecycle::test_an_editor_save_keeps_the_stored_status killed killed
44b the first material edit drops origin.one_time test_workflow_handoff_origin::test_a_material_change_marks_a_hand_off_edited_and_keeps_it_one_time[2] killed killed
45 a v3 save accepts a top-level one_time field test_workflow_handoff_origin::test_a_save_route_create_never_records_one_time[personal-3] killed killed
45b a server create records one_time from the client payload test_workflow_handoff_origin::test_a_chat_proposal_draft_never_records_one_time killed killed
46 the hand-off gate skips the Use Workflow Results In Chat role check (A7) test_orchestration_workflow_handoff_routes::test_only_the_requester_can_read_or_decide_a_handoff killed killed
47 the server create overwrites instead of creating only if absent (A11.5) test_workflow_handoff_builder::test_a_create_that_loses_the_race_returns_the_stored_hand_off[blueprint] killed killed
48 the hand-off creates are not one-time (A11.5) test_workflow_handoff_builder::test_a_hand_off_is_created_once_paused_and_one_time killed killed
49a accept adopts a record at the hand-off id without checking it is a hand-off workflow test_orchestration_workflow_handoff_routes::test_a_workflow_at_the_handoff_id_that_is_not_one_time_is_refused_as_another_kind killed killed
49b ¹ a repeated create adopts a record without checking it is a hand-off workflow (A11.5) test_workflow_handoff_builder::test_a_one_time_record_is_adopted_only_as_this_users_hand_off_from_chat killed killed
50 ¹ the hand-off creates name no actor (A11.5) survives (equivalent: modified_by = actor_user_id or user_id (personal_workflows L896) still records the requester) survived survived
50b the hand-off creates name another actor (A11.5) test_workflow_handoff_builder::test_an_edited_hand_off_stays_a_paused_manual_durable_workflow killed killed
51 ¹ an edited hand-off keeps the payload's URL Access authorization fields (A11.5) survives (equivalent: the build clears URL Access authorization unless URL Access is on, and _requests_url_access refuses that first) survived survived
52 a report page may hold more than 100 saved records test_workflow_handoff_end_to_end::test_an_accepted_handoff_runs_once_and_posts_its_summary_to_chat[two-hundred-documents] killed killed
53 the planner adapter accepts a blueprint with a non-local agent (A14) test_orchestration_workflow_handoff_planner::test_a_hand_off_that_breaks_the_rules_twice_is_dropped_and_reported killed killed
54 a blueprint hand-off may use a non-local agent (A14) test_workflow_handoff_builder::test_only_local_agents_may_run_a_hand_off killed killed
55 ¹ an edited hand-off may use a non-local agent (A14) test_workflow_handoff_builder::test_an_edit_may_not_add_a_hosted_agent_outside_the_loop killed killed
56 the admission reserve is wrong, so the effective loop limit drifts (A15) test_workflow_handoff_builder::test_the_loop_limit_follows_the_admin_setting_up_to_the_hand_off_bound killed killed
57a a lowered loop limit is reported as over the limit instead of changed (A15) test_workflow_handoff_builder::test_the_loop_limits_are_enforced killed killed
57b ¹ the create is not given the loop limit the hand-off disclosed test_orchestration_workflow_handoff_routes::test_an_accept_creates_the_disclosed_bound_and_refuses_a_limit_lowered_below_it killed killed
58a an invalid or unavailable hand-off card gets the ready note test_orchestration_workflow_handoff_adapter::test_the_reply_notes_a_ready_hand_off_and_says_why_another_was_not_handed_off killed killed
58b an invalid or unavailable hand-off card gets no note test_orchestration_workflow_handoff_adapter::test_the_reply_notes_a_ready_hand_off_and_says_why_another_was_not_handed_off killed killed
58c a completed hand-off whose card cannot be described gets no note test_orchestration_workflow_handoff_adapter::test_the_reply_notes_a_ready_hand_off_and_says_why_another_was_not_handed_off killed killed
59 a dropped hand-off is not reported test_orchestration_workflow_handoff_planner::test_a_hand_off_that_breaks_the_rules_twice_is_dropped_and_reported killed killed
60 a model-supplied workflow_handoff_notes is not stripped test_orchestration_workflow_handoff_planner::test_a_model_cannot_report_a_dropped_hand_off[on] killed killed
61 the missing_final_response hand-off steer is missing test_orchestration_workflow_handoff_planner::test_the_missing_answer_steer_is_only_for_a_plan_that_hands_work_off killed killed
62 the delivery log carries the raw run ID (E2E log sweep is live) test_workflow_handoff_end_to_end::test_an_accepted_handoff_runs_once_and_posts_its_summary_to_chat[three-documents] killed killed
alias a hand-off card is offered to a later plan as a saved result test_orchestration_workflow_handoff_adapter::test_the_step_keeps_its_sidecar_on_the_server_and_retains_only_the_card killed killed

82 of 84 killed in both modes; the 2 survivors are equivalent. ¹ Re-run after the set B tests (f31d45f) were added.

Guardrails

Run with run_v2_guardrails.py <worktree> f1aeef13d HEAD 7a, against the merged V2 base f1aeef1. All PASS.

Check Result
Changed files 54; V2 .ts/.tsx files changed: 0
broken-access-control PASS (28 files)
xss-sinks PASS (29 files)
swagger-routes PASS (2 route files; the 4 new routes carry @swagger_route(security=get_auth_security()))
python-syntax PASS (500 files compile)
malicious-pr-review PASS, 0 blockers. 3,456 heuristic findings are marked "Needs investigation", 1,952 of them in the off-golden JSON fixture. The 59 "dynamic execution" markers are word matches on empty and HandoffFixture, plus subprocess in the required fresh-process import tests.
v2-ts-sinks PASS

Decisions made autonomously / deviations

The coordinator approved each of these before I acted on it.

  • A11. Create path. Hand-off creates go through create_personal_workflow_if_absent, not save_personal_workflow, which can't take an origin or a server-chosen id. This deviates from approval note 3.
    • The call names the requester as the actor, so created_by and modified_by are the requester. It uses the id derived from the hand-off, passes a server-built origin with one_time: True, and always overwrites is_enabled with False.
    • Payloads are prepared like Phase 4's on both the as-proposed and edited paths: URL Access authorization fields are popped, URL Access is refused as unsupported_field, a foreign id is refused, and any client origin or one_time is stripped.
    • A repeated create adopts the record it finds only if it's a one-time orchestration workflow belonging to the same user. Otherwise it returns handoff_kind_mismatch.
    • Mutants 47–51 cover this. 50 and 51 are equivalent.
  • A12. Approval-floor pin test edited. Only test_only_starting_a_saved_workflow_sets_an_approval_floor changes. Its floored set becomes {WORKFLOW_RUN, "workflow_handoff"}, with manual for both and None for every other descriptor.
  • A13. No per-item caps (deviates from decision 6). A current-item Analyze can't carry a length bound without failing the item, and under halt the run. The bound therefore rests on the runner.
    • The 200-item end-to-end test asserts the bound on the real reporting path. Mutant 52 (page records raised to 1000) dies.
    • Known limitation: a record too big for the report model fails the run with indivisible_record. 6b-1 posts its one failure note, and the saved records stay.
  • A14. Local agents only. The check runs at planning, at dry run and at accept, by an explicit agent_type check, and refuses with handoff_agent_unsupported (422). At planning it lives in the hand-off-only part of the context, so the shared agent entries and the off-golden don't change. Mutants 53–55 cover it.
  • A15. Loop bound of 2,000. One helper sets the bound everywhere: handoff_effective_loop_limit(settings) = min(HANDOFF_MAX_LOOP_ITEMS, loop item limit). The context, the prompt, validation, the builder and the disclosure all use it.
    • HANDOFF_MAX_LOOP_ITEMS is derived from WORKFLOW_MAX_EXECUTION_ADMISSIONS: two admissions per item, three fixed, and a reserve of 997.
    • Accept reads fresh settings and refuses a limit lowered below the disclosed N with 409 handoff_limit_changed. The run keeps the disclosed N.
    • Runs have a 24-hour deadline. A run that hits it pauses with deadline_exceeded, and 6b-1 treats it as expired.
    • Mutants 56, 57 and 57b cover this.
  • A16. Pause, not stop. An all_matches query that matches more than its limit pauses at the freeze, before any item is analyzed. The pause offers only Cancel, and the deadline doesn't expire it.
    • If the user cancels, the run ends cancelled, one_time_status is written, and chat gets one "was cancelled" note.
    • If nobody cancels, 6b-1 closes it as expired after its grace period: one bell notice, nothing in the chat.
    • The status route shows the paused run as waiting/needs_you with cancel allowed.
    • There's no accept-time count. The draft's For each iterable has the shape POST /api/user/workflows/loop-inputs/preview accepts, so 7b can show the match count before accept.
  • A17. A hand-off isn't a deliverable (D3). A hand-off-only plan declares no deliverables and no final_response. The server adds workflow_handoff_note in _finalize on every finalize status.
    • Degrading mirrors Phase 5: drop_workflow_handoffs, workflow_handoff_notes and "No workflow was handed off."
    • With an enabled hand-off step, the missing_final_response repair text also tells the planner not to answer the handed-off part now.
    • A hand-off-only plan gets no fallback reply.
    • Mutants 39, 40 and 58–61 cover this.

Also approved at planning: A2, the server-only origin.one_time marker.

  • Every client save path strips it.
  • It isn't a material field or part of the M365 fingerprint.
  • An editor save can't clear it.
  • Phase 5's workflow_run refuses a one-time workflow with workflow_one_time.

Contract

Routes

All four routes are under /api/v2/orchestration/runs/<run_id>/workflow-handoffs. Each one carries @swagger_route(security=get_auth_security()), needs a signed-in user with personal workflows (and the WorkflowUser role when that's required), and re-checks every hand-off gate on each request. Anyone other than the requester gets run_not_found.

Method and path Body Returns
GET …/workflow-handoffs?conversation_id= none {run_id, handoffs: [{handoff_id, step_id, state, reason, created_at, expires_at, actions, summary, disclosure, workflow?, run?, chat_delivery?}]}
POST …/workflow-handoffs/<handoff_id>/accept {conversation_id, mode, workflow?} 201 if this request created the workflow, otherwise 200: {handoff_id, state: queued, created, workflow: {id, name, is_enabled}, run: {id, status}, chat_delivery}
POST …/workflow-handoffs/<handoff_id>/deny {conversation_id} 200 {handoff_id, state: denied}; idempotent
GET …/workflow-handoffs/<handoff_id>/draft?conversation_id= none {handoff_id, workflow, url_access_note}; the user's own document and scope ids only, never model_selection or the handle map

Errors

There are 23 closed error codes, never str(exc). The full table is in the feature doc's Errors section.

  • A Phase 4 proposal id sent to a hand-off route gets 409 handoff_kind_mismatch.
  • Phase 4's routes refuse a hand-off id with a new 409, proposal_kind_mismatch, and never take a one-time workflow for a proposal's. This is an additive hunk in functions_orchestration_workflow_proposals.py, away from proposal_status and _workflow_projection (A9).
  • If the workflow was created but its run wasn't queued, the error carries state: created, and a retry queues the same run.
Leak surface (plan §16)
Surface Handling Test
Plan and step stream frames The plan holds handles only, never ids. Step frames go through the existing allowlists and never carry the sidecar. Frame scans for fixture ids and model_selection
Plan payload and run summary Handles only. The summary hides workflow_handoff_decisions. Id scan; asserted absent
Public step record The allowlist drops workflow_handoff (the sidecar) Asserted absent; mutation
Step result (the card) Closed projection: handoff_id, name, summary, disclosure text and counts Id and model_selection scan
Status, accept and deny bodies Closed fields. The workflow and run ids are the user's own, and 7b needs them. Closed-key assertions
Draft Like Phase 4: the user's own document and scope ids; never model_selection or the handle map Keys asserted absent
Error bodies Closed ERROR_MESSAGES; never str(exc) Exception sentinel never in a body; mutation
Logs log_event with codes, hashed identifiers, and sanitize_log_message for anything user-derived Log-capture scans, including the E2E sweep from accept to delivery
Durable run record chat_invocation holds the user's own conversation, run and step ids and handoff_id, visible in their run history as in Phase 5. chat_delivery stays on the server (6b-1). 6b-1 status projection unchanged
6b-1 status route Unchanged four-field projection Lists the hand-off run
Planner prompt Handles and display names only Id scan
Created workflow Opens in the user's own editor with their own document and scope ids, like Phase 4 references n/a

Notes

  • LF fix (15f1e50). ce16933 had stored functions_workflow_chat_delivery.py, functions_workflow_reporting.py and the end-to-end test with CRLF line endings. The two production files match V2 byte for byte again and aren't in this diff.
  • Test-harness pollution fix (bacbf0a), present on the base too. SaveParityHarness in test_workflow_draft_save_parity.py left modules that the real stores import, such as functions_workflow_chat_delivery, bound to its test doubles in sys.modules.
    • In one combined pytest process, that caused 4 extra failures: 2 hand-off routes tests, 1 lifecycle test, and test_workflow_loop_schema.py::test_group_iterable_scope_is_bound_to_the_server_normalization_context.
    • The harness now undoes every sys.modules change, and two guard tests in test_workflow_draft_service.py enforce that.
    • Before the fix, the combined run had 8 failures; now it has only the 4 base ones.
  • Overlap with draft V2 workflow alerts: require acknowledgment, repeating sound, size options and team delivery (0.261.235) #1636. It shares 4 files with this PR (admin_settings_fields.py, functions_personal_workflows.py, functions_settings.py and route_frontend_admin_settings.py) and auto-merges cleanly. If it lands first, I'll merge V2 and re-run the admin-settings, test_orchestration_actions_admin.py, cap-count and hand-off settings suites in 4 modes.

Follow-ups

  • 7b: the V2 hand-off card (accept, edit, decline), with the match-count preview through the existing loop-inputs preview route.
  • Floor-refusal wording: a hand-off plan refused at run time for losing its approval floor shows Phase 5's text from _require_approval_floor: "A plan that starts a saved workflow runs only when you run it yourself…". A hand-off-specific message means changing shared plan-revision code.
  • Silent gate: when a gate fails, the capability is left out without a reason, so the planner can't say why hand-off isn't available.
  • Daily limit: it counts existing hand-off workflows. That isn't transactional, so two accepts at the same moment can both pass, and a deleted hand-off stops counting.

Documentation

  • Release notes updated (docs/explanation/release_notes.md, v0.261.238)
  • Feature documentation updated: new docs/explanation/features/CHAT_ORCHESTRATION_WORKFLOW_HANDOFF.md, docs/admin/orchestration.md and docs/_data/features.yml, plus the regenerated docs/_data/app_surface.yml
  • Fix documentation not needed

Security checklist

  • New Flask routes include @swagger_route(security=get_auth_security()) (all 4; the swagger-routes guardrail passes)
  • Settings sent to non-admin frontends use sanitize_settings_for_user(). No new non-admin settings output; the two settings render only on the admin pane.
  • Browser JavaScript is served from local SimpleChat static assets only; no CDN-hosted JS. No browser JavaScript changes.
  • No secrets, keys, connection strings, or local-only artifacts are included

Head SHA: d2af803b1dfaa565e3f16feb16c00a13c1cd2192

Paul Lizer (paullizer) and others added 23 commits October 3, 2026 02:26
Pins planning, capability resolution, the browser capability list, and the Phase 4 and Phase 5 route responses for every hand-off gate variant, captured on the unmodified base before any hand-off change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
… gates

- New default-off enable_chat_orchestration_workflow_handoff key and a per-user
  daily hand-off limit (default 5, 1-100) on the orchestration admin tab.
- Server-only origin.one_time marker and a one_time_status runtime field.
- workflow_handoff capability: Reason role, manual approval floor,
  max_per_plan 1, dormant until the key is on and silent when unusable, so
  every existing projection and prompt stays byte-identical.
- Hand-off gates in the workflow planning context module.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 7a (#1549). A pure builder turns a hand-off blueprint (a loop over named
documents or a bounded workspace query, a per-item review task and a report
task) into a definition_version 3, durable_execution workflow with a
current-item Analyze, a collect node and a report task. The draft service
gains a closed hand-off schema, handle normalization, dry run, create and
edited-payload create paths. Hand-offs are created paused and one-time, with
notify-only alerts and no M365 or Run as. Phase 4's blueprint, payloads and
dry runs are unchanged (pinned by digests).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Captured with --write-golden on unmodified ca95275 (0.261.229, #1630) in a fresh detached worktree. The only difference from the fd842e1 capture is the captured_from label; #1630 changed no captured byte.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds the workflow_handoff adapter and planner helpers, the hand-off plan rules (static input, no consumers, exclusive with workflow proposals and runs, and the plan budget code offered only with hand-off), the hand-off planning marker in the workflow planning context, and the scope seeds the plan route passes to it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The fixture was written again with --write-golden from the unmodified V2 commit f7e268c (0.261.233). Apart from the captured_from label it is byte-identical to the ca95275 capture, so the V2 merge changed nothing the golden compares.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds the hand-off instructions, the Plan limits section, the budget and
hand-off repair hints and the degrade path, all only when the request
offers workflow_handoff, plus the gated deliverables fact and recipe.
The off-golden harness now uses the real correlation helper so it holds
when an earlier suite imported the planner under a stand-in logger.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds functional tests for the hand-off planner guidance: the Plan limits section, the budget repair round, the answer steer, the drop report and the manual approval floor. Plan-limit lines now use singular nouns for a count of one. Fixes the builder test header order.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The executor deep-copies the adapter's workflow_handoff sidecar into the
server step record, restores it on recovery like the proposal, run and
results sidecars, and leaves it out of the public step record and the
stream frames. The finalize note covers a prepared or refused hand-off,
and the services alias discovery skips hand-off steps.

Adds the adapter, capability and fresh-process import tests.

Refs #1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A structured (v3) run is still refused unless it is a chat hand-off run whose single workflow output is the report node's text output; that report is read through the node-level result store, which re-proves the run's definition identity.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Add the requester-only workflow-handoffs route family: status, draft, accept and deny.
- Accept re-authorizes, rebuilds and revalidates the blueprint, claims the decision with the
  run's CAS model, creates the one-time workflow disabled and queues exactly one durable run
  with chat delivery. A created-but-not-queued retry reuses the same workflow and run IDs.
- Refuse a hand-off on Phase 4's accept and a Phase 4 proposal on the hand-off accept.
- Phase 5's workflow_run refuses a one-time workflow with the closed workflow_one_time reason.
- Share the chat delivery seed between Phase 5 and the hand-off queue.
- Exclude hand-offs from Phase 4's cap and add the rolling daily hand-off count.
- Add route, route-policy, import and Phase 5 refusal tests.

Refs #1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Pins the one_time origin marker outside the revision and fingerprint, refuses an edited one-time workflow in Phase 5, proves a create that loses the race returns the stored hand-off, and checks edited hand-off parity with a save.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
On every terminal transition of the run a one-time workflow was handed off
for, the runtime projection now writes one_time_status {state, run_id,
completed_at} beside the other runtime fields. It stays outside the
definition revision and the execution fingerprint, never changes
modified_at or is_enabled, and a failure while computing it is logged
content-free and never stops the run from being finalized.

The lifecycle test pins every terminal state, the failing-write path, the
runs that record nothing, a resumed run, editor saves, the query-over-limit
pause the deadline does not expire, and how a disabled hand-off ends in chat
through 6b-1.

Refs #1549

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Drives an accepted hand-off over 3 and 200 workspace documents through the real durable runtime, workflow runner, result store, reporting, result reader and 6b-1 delivery worker, faking only the containers, the model, the per-document Analyze, document enumeration and the lease heartbeat. Every app module's log_event is recorded, and no log carries a private value or an internal hand-off ID.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Captured on unmodified f1aeef1 (0.261.233). The planning and route captures are byte-identical to the f7e268c capture; only the captured_from label changes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Commit ce16933 stored functions_workflow_chat_delivery.py, functions_workflow_reporting.py and the hand-off end-to-end test with CRLF line endings. The two production files now match V2 byte for byte again, and the test keeps only its comment change.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
SaveParityHarness loads the real workflow stores with test doubles installed in sys.modules. Its _installed() helper put back only the names it listed, so a module the real stores imported in turn, such as functions_workflow_chat_delivery, stayed in sys.modules bound to the doubles, and later tests in the same pytest process imported it. _restored_modules() now undoes every sys.modules change made while loading. Two guard tests in test_workflow_draft_service.py fail if building or calling a harness leaves any module entry changed, or if a module imported before a harness ran is not put back.

The pollution exists on the base. On 15f1e50 the combined pytest run fails 8 tests in both pytest and pytest -O: the 4 known proposal_editor_round_trip failures plus 4 caused by the leaked modules (2 hand-off routes tests, 1 lifecycle test and test_workflow_loop_schema.py::test_group_iterable_scope_is_bound_to_the_server_normalization_context). With this change only the 4 base failures remain (1489 passed). The standalone python and python -O runs pass before and after.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds the feature doc, the admin settings and limits rows and troubleshooting entries, the release notes entry, the features.yml setting key and the regenerated docs inventory. test_docs_app_surface_coverage.py 7/7 and test_docs_site_quality.py 6/6 pass.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds four tests: a one-time workflow at a proposal's id is never taken for the proposal's workflow (3b); an accept creates the disclosed bound and refuses a loop limit lowered below it (57b); a one-time record is adopted only as this user's hand-off from chat (49b); an edit may not add a hosted agent outside the loop (55). builder+routes and routes+builder 93 passed and harness8 285 passed under pytest and pytest -O; the combined hand-off, phase 4/5 and loop-schema run shows only the 4 base proposal_editor_round_trip failures (1493 passed).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sets config.py VERSION to 0.261.238, one above the versions claimed by the open V2 PRs (#1636 0.261.234, #1637 0.261.235, #1638 0.261.236, 6b-2 next), and moves the hand-off modules' and tests' Version, Implemented in, MINIMUM_VERSION and assert_app_version_at_least literals to it. The off-golden fixture keeps its factual capture note (f1aeef1, 0.261.233).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
"""Read the rolling 24-hour hand-off limit; a corrupt stored value fails closed."""
if settings is None:
# Settings initialize application clients; load them only at a request boundary.
from functions_settings import get_settings
_require("Prepared a workflow hand-off." in " ".join(_messages(calls, logging.INFO)), "The step must log.")
_assert_logs_clean(calls, sidecar)

calls = _install(monkeypatch, ho, drafts, outcome={"ok": False, "errors": [{"code": "scope_unavailable"}]})
touched = []

class _Forbidden(types.ModuleType):
def __getattr__(self, name):
SECRET_REFERENCE = {"id": "secret", "name": "secret", "document_id": "forbidden-doc", "scope_type": "personal"}
RACE_CREATES = {
"blueprint": lambda harness, payload: _create(harness, DOCS, DOC_HANDLES),
"payload": lambda harness, payload: _create_from_payload(harness, payload),
Paul Lizer (paullizer) and others added 6 commits October 5, 2026 13:03
V2 moved to efee707 (VERSION 0.261.234) when #1635 merged: saved-result
error messages by family and repaired workflow test harnesses.

Conflicts:
- config.py: keep this branch's 0.261.238, still above V2's 0.261.234 and
  the claimed 0.261.235 to 0.261.237.
- release_notes.md: keep both sections, 0.261.238 above 0.261.234.

test_workflow_draft_save_parity.py auto-merged: #1635 adds the chat delivery
contract to REAL_MODULES, and this branch's _restored_modules still undoes
every sys.modules change made while the harness loads.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
#1635 loads the chat delivery contract as a real module, so it no longer
reaches the harness only as a transitive import. The comment now describes
what _restored_modules undoes without naming that module.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 moved to 5be1d84 (VERSION 0.261.235) when #1636 merged: must-acknowledge
workflow alerts, alert sounds, sizes and team delivery.

Conflicts:
- config.py: keep this branch's 0.261.238, still above V2's 0.261.235.
- release_notes.md: keep both sections, 0.261.238 above 0.261.235. This also
  restores V2's 0.261.234 entry exactly: the previous merge (ab87e79) left
  extra spaces in its Ref line and dropped the blank line before 0.261.233.

admin_settings_fields.py, functions_personal_workflows.py, functions_settings.py,
route_frontend_admin_settings.py and docs/_data/features.yml auto-merged.
docs/_data/app_surface.yml auto-merged with capabilities: 125, because both
sides raised the same count by one. Regenerated with
scripts/build_docs_inventory.py: 126, for enable_chat_orchestration_workflow_handoff
and enable_workflow_alert_sounds.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The adapter test was the only new hand-off test file without one, so its
python and python -O runs executed nothing. It now runs its tests through
pytest.main, like the other hand-off test files.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The V2 admin renderer evaluates each field's conditions on their own, so a field
gated on another switch must repeat that switch's conditions. The daily limit
was gated on the hand-off switch but not on the switch's own
allow_user_workflows gate, so it could stay on screen while the switch was
hidden. Caught by test_v2_admin_settings_schema.py's
test_gated_fields_inherit_their_gate_s_own_conditions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 moved to 25470e7 (VERSION 0.261.236) when #1643 merged: the V2 chat
rail scrolls as one panel, and workflow alerts pop up from the bell.

Conflicts:
- config.py: keep this branch's 0.261.238, still above V2's 0.261.236.
- release_notes.md: keep both sections, 0.261.238 above 0.261.236.

#1643 shares no other file with this branch. Apart from config.py, the merged
tree differs from ac9e524 only in #1643's 15 files, each identical to V2's.
docs/_data/app_surface.yml is unchanged after re-running
scripts/build_docs_inventory.py.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) and others added 4 commits October 5, 2026 18:30
V2 moved to 4f3eacb (VERSION 0.261.238) when #1644 merged: the
orchestration settings store returns plain dicts, which fixes the settings
race, and Microsoft 365 actions now work as chat orchestration plan steps.

Conflicts:
- release_notes.md: keep both sections. This branch's hand-off section sits
  above #1644's; both are headed 0.261.238 until the renumber commit that
  follows moves this branch to 0.261.239.

config.py auto-merged: both sides were at 0.261.238.
functions_orchestration_adapters.py, functions_orchestration_schema.py and
docs/admin/orchestration.md auto-merged; each side's changed lines are kept
unchanged. The merged tree differs from 6fdef66 only in #1644's files;
every file outside those three and the release notes is identical to V2's.
docs/_data/app_surface.yml is unchanged after re-running
scripts/build_docs_inventory.py.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 reached 0.261.238 when #1644 merged, so this branch moves one above it.
Sets config.py VERSION to 0.261.239 and moves this branch's own 0.261.238
lines to 0.261.239: the hand-off modules' and tests' Version, Implemented in,
MINIMUM_VERSION and assert_app_version_at_least literals, the two route-test
coverage notes, the run adapter test's hand-off refusal note, the two
hand-off rows in docs/admin/orchestration.md, the feature doc and this
branch's release-notes header. #1644's own 0.261.238 lines (its schema
docstring, its Microsoft 365 section and troubleshooting row in
orchestration.md, its fix docs and its release-notes section) are unchanged.
The open V2 PRs claim 0.261.234 (#1639), 0.261.235 (#1637), 0.261.236
(#1638) and 0.261.246 (#1641), none of them 0.261.239.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Merge only landed V2 with rerere disabled. Preserve both capability-import sets, both draft error families, all incoming release-note sections and the three V2 off-golden fixtures. Keep V2 VERSION 0.261.248 pending the separate hand-off renumber to 0.261.250. Auto-merged shared files preserve both sides; executor registry imports are their exact union. The regenerated docs inventory is unchanged. Full verification remains pending.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
V2 reached 0.261.248 with #1641; coordinator reserves 0.261.249 for 6b-2 and 0.261.250 for Phase 7a. Change only config.py VERSION and the 44 PR-owned version lines. Preserve every incoming V2 version reference and golden fixture. No behavior changes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@paullizer
Paul Lizer (paullizer) marked this pull request as ready for review October 6, 2026 01:43
@paullizer
Paul Lizer (paullizer) merged commit 15feec6 into paullizer-react-v2-ui Oct 6, 2026
11 checks passed
Paul Lizer (paullizer) added a commit to paullizer/simplechat that referenced this pull request Oct 6, 2026
Take V2's config.py (0.261.250) and keep 6b-2's release-note section above V2's 0.261.250 section with every V2 byte preserved. The renumber to 0.261.251 follows separately.

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 6, 2026
Keep all base version history; update only 6b-2's version, tests and documentation after the V2 microsoft#1640 merge, which took 0.261.250: config.py moves from 0.261.250 and 51 lines in 21 other files move from 0.261.249, all to 0.261.251.

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 6, 2026
The hand-off branch of read_workflow_result loaded the report manifest by its exact node
selectors and checked its identity, text output and completion, but never walked its lineage.
A report whose consumed-input receipts were malformed, or named a missing or corrupt parent,
still read as available, although the general path refuses the same lineage.

_read_handoff_result now calls the shared node lineage authorizer
(authorize_workflow_node_result_read) on the saved workflow, seeded with the loaded manifest and
the same memoized loader, before the result is described or excerpted. Sources aren't
re-resolved. read_workflow_result passes through the user id it has already proved.

The reader tests add a strong valid control whose report consumed a real collect records result,
built through the runner's result contract, and cover a malformed receipt (the repro), a missing
parent, a parent whose output disagrees with the receipt, a parent whose bytes don't match its
hash, and edited or re-enabled definitions with lineage. A descriptor-only read walks the
lineage but reads no section.

Refs microsoft#1549, microsoft#1543. Follow-up to microsoft#1640.

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 6, 2026
Add the Workflow Hand-off Result Lineage Fix doc, covering the reader's
missing lineage walk, the change and its failure codes, the recaptured
off-golden, validation and limitations.

In the hand-off feature doc, the reader now re-proves the report's lineage
with the shared node lineage authorizer, an edited or re-enabled workflow
still fails closed with workflow_result_invalid, and the test, file and
performance notes match.

Add a 0.261.252 release-notes section with two bug fixes. Existing
sections are unchanged, and the docs inventory regenerates unchanged.

Part of microsoft#1549. Part of microsoft#1543. Follow-up to microsoft#1640.

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 6, 2026
test_the_phase_4_blueprint_and_payloads_are_unchanged failed at V2
15feec6 on the schema digest alone. Its pin (ab12fb1d) predated microsoft#1641,
which added the merge task schema and was merged into Phase 7a at
b8e4730 without a refresh.

The pin now holds 520139cb, the value V2 computes at a5a5b1c, after microsoft#1641
and before hand-off merged, so the test still proves hand-off left the
Phase 4 schema unchanged. The payload, validation and dry-run pins are
unchanged.

The fix doc adds the builder test to Files modified, records the pin under
Validation, and gives the 200-document read time as about 1.7 seconds, to
match the c07b911 timings.

Refs microsoft#1549, microsoft#1543. Follow-up to microsoft#1640.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Paul Lizer (paullizer) added a commit that referenced this pull request Oct 6, 2026
…w-up

- Status date 2026-10-06, with paullizer-react-v2-ui at 0.261.252.
- Phase 6: 6b-2 done in #1639 (v0.261.251), with how it shipped.
- Phase 7: 7a done in #1640 (v0.261.250) and its lineage follow-up in #1647
  (v0.261.252); 7b, the V2 hand-off card, is next. Admins should leave the
  hand-off setting off until 7b ships.
- Section 9: the run deep-link and one-time workflow decisions are settled.
- Release-notes integrity follow-up updated to 187 missing releases.

Docs only; no version bump.

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