Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
1acce9f
Add enable_workflow_plan_replay setting (WIP)
paullizer Oct 8, 2026
fbc4fb3
Add plan replay module: allowlist, refusals, freeze contract and pref…
paullizer Oct 8, 2026
38cd052
Resolve plan replay module name checks (WIP)
paullizer Oct 8, 2026
cf945b4
Pass the plan contract version to the replay capability check (WIP)
paullizer Oct 8, 2026
c3536ee
Add plan replay freeze, builder, executor adapter and typed result (WIP)
paullizer Oct 8, 2026
37d2fea
Re-read the stored workflow before each plan replay run
paullizer Oct 8, 2026
a572f9b
Route plan replay tasks through the frozen-plan executor
paullizer Oct 8, 2026
cb5b7ca
Keep the orchestration scheduler away from workflow plan replays
paullizer Oct 8, 2026
7183f43
Add saved-plan preview and save routes; let the workflow run alone dr…
paullizer Oct 8, 2026
552e60a
Plan replay: browser-safe frozen plan compare and cadence floor on edit
paullizer Oct 8, 2026
0720d87
Map saved-plan workflow validation errors to their public message
paullizer Oct 8, 2026
b646b7a
Drop unused plan replay import from the workflow runner
paullizer Oct 8, 2026
14ad5fb
Keep the plan replay import lazy and re-check sources when saving
paullizer Oct 8, 2026
576d564
Refuse only file deliverables and add the plan replay freeze tests
paullizer Oct 8, 2026
b1e8604
Test headless plan replay execution, cancel, budget and lease loss
paullizer Oct 8, 2026
3e18bf9
Test plan replay preview, save and chat-route guards
paullizer Oct 8, 2026
e969104
Add plan replay import and scheduler boundary tests
paullizer Oct 8, 2026
79b6dc9
Store a typed plan replay projection on the task run item
paullizer Oct 8, 2026
a2eec80
Add the V2 plan replay card, read-only editor summary and inspector
paullizer Oct 8, 2026
5615991
Add plan replay off-golden test pinned to the base commit
paullizer Oct 8, 2026
8ff307f
Re-check orchestration and the admin capability list through the repl…
paullizer Oct 8, 2026
dfc2b3b
Check the chat cadence floor when a saved plan's schedule is saved again
paullizer Oct 8, 2026
6879721
Re-check a stored plan for a user question before every replay run
paullizer Oct 8, 2026
21776dc
Run plan replay workflows under the durable workflow lease
paullizer Oct 8, 2026
33e1069
Reconcile earlier replay attempts and settle stopped ones
paullizer Oct 8, 2026
71dffe0
Test that running a replay workflow queues one durable run
paullizer Oct 8, 2026
999a5d4
Test durable replay leases, crash reconciliation and self-settle
paullizer Oct 8, 2026
026c7f8
Test the runner's durable replay unit and attempt numbering
paullizer Oct 8, 2026
1d544fb
Fix doubled step prefix in plan replay refusal notes
paullizer Oct 8, 2026
86d2dce
Document workflow plan replay
paullizer Oct 8, 2026
be8a302
Label the plan replay roles design as option (c)
paullizer Oct 8, 2026
8f0d5b3
Bump version to 0.261.303
paullizer Oct 8, 2026
92f8fa1
Merge origin/paullizer-react-v2-ui into paullizer-plan-replay-workflo…
paullizer Oct 8, 2026
efe2eda
Refuse 6c waited plans as replay_wait_unsupported
paullizer Oct 8, 2026
9f87bf1
Give replay workflows a default 'Run failed' alert
paullizer Oct 8, 2026
90947b2
Bump version to 0.261.305 for plan replay
paullizer Oct 8, 2026
ed7d591
Merge V2 817f9dd07 (#1726) into the plan-replay branch
paullizer Oct 8, 2026
4ba8c12
Bump plan replay to VERSION 0.261.306
paullizer Oct 8, 2026
18a1605
Merge V2 587094fb6 (#1727, #1728, #1730) into the plan-replay branch
paullizer Oct 8, 2026
964c5c5
Bump plan replay to VERSION 0.261.307
paullizer Oct 8, 2026
b210aaa
Merge V2 11d84783e catalogue catch-up into plan replay
paullizer Oct 9, 2026
3bd32b0
Restore VERSION 0.261.307 after the V2 catch-up
paullizer Oct 9, 2026
ea2adaa
Merge V2 08def99f7 pending-action cards into plan replay
paullizer Oct 9, 2026
4337887
Reuse the empty pending-action stub in replay card tests
paullizer Oct 9, 2026
9c18dbf
Bump plan replay to VERSION 0.261.308
paullizer Oct 9, 2026
8d39556
Address plan replay CodeQL quality findings
Copilot Oct 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions application/single_app/admin_settings_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -4382,6 +4382,29 @@
{"key": "allow_user_workflows", "equals": True},
],
},
{
"key": "enable_workflow_plan_replay",
"type": "switch",
"label": "Repeat Chat Plans On A Schedule",
"help": (
"Lets a user save a completed plan from their own private conversation as a personal "

Check warning on line 4390 in application/single_app/admin_settings_fields.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
"workflow that repeats the same steps on a schedule, such as every Monday. The user "
"sees every step and the schedule before creating it. Each run replays the saved "
"steps exactly, as that user, in the workflow's own conversation, and checks the "

Check warning on line 4393 in application/single_app/admin_settings_fields.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
"user's access again first. Only steps that search, analyze, compare or merge "

Check warning on line 4394 in application/single_app/admin_settings_fields.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
"documents, inspect tabular files, write the answer or generate images can be "
"repeated. Web search and other steps that need a signed-in session can't be "

Check warning on line 4396 in application/single_app/admin_settings_fields.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
"repeated. Off by default, because a saved plan "
"keeps running with the user's access. Turning it off makes existing repeat "
"workflows stop with a clear reason instead of deleting them. Requires Chat "
"Orchestration and Enable Personal Workflows."
),
"default": False,
"depends_on": [
{"key": "enable_chat_orchestration", "equals": True},
{"key": "allow_user_workflows", "equals": True},
],
},
{
"key": "chat_orchestration_enabled_capabilities",
"type": "checkbox_set",
Expand Down
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.307"
VERSION = "0.261.308"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
8 changes: 8 additions & 0 deletions application/single_app/functions_group_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,14 @@ def build_group_workflow_document(group_id, workflow_data, actor_user_id, user_i
and ``origin`` belong to a server create path, never to a save payload.
"""
workflow_data = workflow_data if isinstance(workflow_data, dict) else {}
if workflow_data.get('plan_replay') is not None or any(
isinstance(task, dict) and task.get('type') == 'plan_replay'
for task in (workflow_data.get('tasks') if isinstance(workflow_data.get('tasks'), list) else [])
):
# Plan replay is personal-only; importing here avoids a workflow-store import cycle.
from functions_workflow_plan_replay import PlanReplaySaveError

raise PlanReplaySaveError('group_not_supported')
settings_options = {'settings': settings} if settings is not None else {}
settings = get_settings() if settings is None else settings
now_iso = _utc_now_iso()
Expand Down
4 changes: 4 additions & 0 deletions application/single_app/functions_orchestration_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ def effective_capability_ids(values):
# workflow. Hand-off also needs proposals, runs and results turned on, because it creates a
# workflow, starts its one run and posts that run's result back into the chat.
WORKFLOW_HANDOFF_SETTING = 'enable_chat_orchestration_workflow_handoff'
# The settings key that must be exactly True before a completed plan can be saved as a personal
# workflow that replays it on a schedule, and before an existing replay workflow may run. It is not a
# capability: a replay is never a step the planner can choose, only a user's explicit save.
WORKFLOW_PLAN_REPLAY_SETTING = 'enable_workflow_plan_replay'
# The tasks a workflow proposal may hold, and the action kinds the planner may say a task needs.
# The workflow planning context and the deliverables import these, so each has one definition.
# The draft service's own task limit (functions_workflow_drafts.BLUEPRINT_MAX_TASKS) stays a
Expand Down
6 changes: 6 additions & 0 deletions application/single_app/functions_orchestration_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ def enumerate_due_runs(container, *, now=None, limit=64):
"AND (NOT IS_DEFINED(c.checkpoints_deleted) OR c.checkpoints_deleted != true) "
"AND (NOT IS_DEFINED(c.outputs_deleted) OR c.outputs_deleted != true) "
"AND (NOT IS_DEFINED(c.superseded_by_run_id) OR IS_NULL(c.superseded_by_run_id)) "
# A saved-workflow plan replay is driven by its workflow run, never by this scheduler.
"AND (NOT IS_DEFINED(c.workflow_replay) OR IS_NULL(c.workflow_replay)) "
"AND (NOT IS_DEFINED(c.latest_attempt_run_id) OR IS_NULL(c.latest_attempt_run_id) "
'OR c.latest_attempt_run_id = "") '
"AND (NOT IS_DEFINED(c.execution_lease) OR IS_NULL(c.execution_lease) "
Expand Down Expand Up @@ -454,6 +456,10 @@ def _process_run(self, selector):
lease = None
try:
record = self.read_run(selector)
if record.get("workflow_replay"):
# The owning workflow run drives, cancels and fences its plan replay.
self.result["runs"].append({**selector, "action": "deferred", "state": "workflow_replay"})
return
states = {row["step_id"]: row["status"] for row in record.get("execution_steps") or []}
execution_work = any(
step.get("enabled", True) and step["role"] != "render"
Expand Down
44 changes: 40 additions & 4 deletions application/single_app/functions_personal_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,7 @@ def get_due_personal_workflows(limit=20):

def build_personal_workflow_document(user_id, workflow_data, actor_user_id=None, *, settings=None, workflow_id=None,
origin=None, user_settings_reader=None, resolve_document=None,
sanitize_source=None, one_time=False):
sanitize_source=None, one_time=False, plan_replay_task=None):
"""Normalize and authorize a personal workflow exactly as saving it would, without writing.

Returns ``(workflow, existing_workflow)``: the document a save persists and the stored workflow
Expand Down Expand Up @@ -937,6 +937,37 @@ def build_personal_workflow_document(user_id, workflow_data, actor_user_id=None,
existing_workflow = get_personal_workflow(user_id, workflow_id) if workflow_id else None
refuse_save_of_deleted_workflow(cosmos_personal_workflows_container, user_id, workflow_data, existing_workflow)

incoming_tasks = workflow_data.get('tasks') if isinstance(workflow_data.get('tasks'), list) else []
existing_plan_replay_task = next((
task for task in (existing_workflow or {}).get('tasks') or []
if isinstance(task, dict) and task.get('type') == 'plan_replay' and isinstance(task.get('plan_replay'), dict)
), None)
incoming_replay_task = any(
isinstance(task, dict) and task.get('type') == 'plan_replay'
for task in incoming_tasks
)
has_replay_payload = (
plan_replay_task is not None
or existing_plan_replay_task is not None
or workflow_data.get('plan_replay') is not None
or incoming_replay_task
)
attach_plan_replay = None
if has_replay_payload:
# Plan replay is a server-owned task envelope; importing here avoids a workflow-store import cycle.
from functions_workflow_plan_replay import (
PlanReplaySaveError,
attach_plan_replay,
normalize_plan_replay_update,
)

if plan_replay_task is not None and existing_workflow is not None:
raise PlanReplaySaveError('plan_replay_read_only')
if existing_plan_replay_task:
workflow_data = normalize_plan_replay_update(existing_workflow, workflow_data)
elif plan_replay_task is None:
raise PlanReplaySaveError('plan_replay_read_only')

workflow_name = _normalize_text(workflow_data.get('name'), 'Workflow name', required=True)
description = _normalize_text(workflow_data.get('description'), 'Description')
file_sync = _normalize_file_sync_config(
Expand Down Expand Up @@ -970,6 +1001,10 @@ def build_personal_workflow_document(user_id, workflow_data, actor_user_id=None,
),
default_document_action=document_action,
)
if plan_replay_task is not None:
tasks = [attach_plan_replay(tasks[0], plan_replay_task)]
elif existing_plan_replay_task:
tasks = [attach_plan_replay(tasks[0], existing_plan_replay_task['plan_replay'])]
definition_fields = normalize_workflow_definition(
workflow_data, existing_workflow, tasks, user_id=user_id,
)
Expand Down Expand Up @@ -1068,7 +1103,8 @@ def build_personal_workflow_document(user_id, workflow_data, actor_user_id=None,
workflow_data.get('schedule'),
existing_workflow=existing_workflow,
settings=settings,
**({'orchestration': True} if origin is not None else {}),
# A saved chat plan keeps the chat-workflow floor whenever its schedule is saved.
**({'orchestration': True} if origin is not None or existing_plan_replay_task is not None else {}),
)

workflow = {
Expand Down Expand Up @@ -1166,7 +1202,7 @@ def save_personal_workflow(user_id, workflow_data, actor_user_id=None):

def create_personal_workflow_if_absent(user_id, workflow_data, *, workflow_id, origin, actor_user_id=None,
settings=None, user_settings_reader=None, resolve_document=None,
sanitize_source=None, one_time=False):
sanitize_source=None, one_time=False, plan_replay_task=None):
"""Create a personal workflow under a server-chosen id and origin, at most once.

Chat orchestration derives ``workflow_id`` from the proposal the user accepted, so accepting the
Expand All @@ -1190,7 +1226,7 @@ def create_personal_workflow_if_absent(user_id, workflow_data, *, workflow_id, o
user_id, {} if workflow_data is None else workflow_data, actor_user_id,
settings=settings, workflow_id=workflow_id, origin=origin,
user_settings_reader=user_settings_reader, resolve_document=resolve_document,
sanitize_source=sanitize_source, one_time=one_time,
sanitize_source=sanitize_source, one_time=one_time, plan_replay_task=plan_replay_task,
)
record, created = create_workflow_definition_record_if_absent(
cosmos_personal_workflows_container, user_id, workflow,
Expand Down
2 changes: 2 additions & 0 deletions application/single_app/functions_saved_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -1950,6 +1950,8 @@
for field in ("output_summary", "response_preview", "reply"):
if field in redacted:
redacted[field] = UNVERIFIED_WORKFLOW_OUTPUT_MESSAGE
# A saved chat plan's typed projection carries the answer text, so it is withheld with the preview.
redacted.pop("plan_replay", None)

Check warning on line 1954 in application/single_app/functions_saved_analysis.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
if redacted.get("error"):
redacted["error"] = UNVERIFIED_WORKFLOW_OUTPUT_MESSAGE
redacted["analysis_access_available"] = False
Expand Down
10 changes: 10 additions & 0 deletions application/single_app/functions_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,10 @@ def get_settings(use_cosmos=False, include_source=False):
# user approves on the plan and again on a hand-off card. Off by default and separate from
# proposals and runs, so an upgrade never turns it on by itself.
'enable_chat_orchestration_workflow_handoff': False,
# Lets a user save a completed chat plan as a personal workflow that replays the frozen plan
# as that user on a schedule. Off by default; turning it off makes existing replay
# workflows refuse to run with a fixed reason rather than deleting them.
'enable_workflow_plan_replay': False,
# Workflows that a chat plan creates for a user: how many one user may hold, and the
# shortest interval one may run on (also never shorter than the general workflow minimum).
'chat_orchestration_max_workflows_per_user': CHAT_ORCHESTRATION_MAX_WORKFLOWS_DEFAULT,
Expand Down Expand Up @@ -2476,6 +2480,12 @@ def update_settings(new_settings, *, expected_etag=None):
new_settings['enable_chat_orchestration_workflow_handoff'] is True
),
}
if isinstance(new_settings, dict) and 'enable_workflow_plan_replay' in new_settings:
# Only a real boolean true lets chat plans be repeated on a schedule; anything else saves as off.
new_settings = {
**new_settings,
'enable_workflow_plan_replay': new_settings['enable_workflow_plan_replay'] is True,
}
if isinstance(new_settings, dict) and 'chat_orchestration_max_workflow_handoffs_per_day' in new_settings:
new_settings = {
**new_settings,
Expand Down
Loading
Loading