Repository navigation
Description
Problem
durable_workflow.history_bundle_verify (published 2.12.3, and source at main 9094996) reports a retention-pruned history-export bundle as status: ok with zero findings. PHP Core's BundleIntegrityVerifier fails the same bundle with the error history.details_pruned. Python replay verification would therefore accept a bundle that has no history to replay or import.
Parity defect filed at General's request from durable-workflow/workflow#856 (PHP fix in draft PR durable-workflow/workflow#980). The PHP fix is not yet released; the bundle below was produced by the #980 head (2e562997).
Bundle contract (additive, from workflow#980)
After WorkflowRunRetentionCleanup::pruneRun(), HistoryExport::forRun() exports a pruned terminal run with:
- top-level
history_complete: false, even thoughworkflow.statusstays terminal - empty
history_events workflow.details_state: "pruned"("retained"on every other bundle) andworkflow.details_pruned_at(a UTC timestamp, or null)dedupe_key= the pre-prune key +:details-pruned:{details_pruned_at}
Exact reproduction
- Produce the bundle with the PHP Core #980 head, in a PHP 8.4 / PostgreSQL 17 / Redis 7 container. A real
TestGreetingWorkflowrun completed (workflow → activity → workflow), was pruned, then exported. The full bundle is below. - Verify it with the normally installed SDK:
docker run --rm -v "$PWD/pruned-bundle.json:/b.json:ro" python:3.12-slim sh -c '
pip install -q durable-workflow==2.12.3
cd /tmp && python -I -c "
import json, durable_workflow.history_bundle_verify as m
r = m.verify_bundle_json(open(\"/b.json\").read())
print(r[\"status\"], json.dumps(r[\"summary\"]), json.dumps(r[\"findings\"]))"'Actual (Python 2.12.3):
ok {"findings": 0, "errors": 0, "warnings": 0, "info": 0, "findings_by_rule": {}} []
The checksum matches: 8fa57d07aa2835e1210f3d5a840a2d46e63b4d918e15689dd8818f9c7b1101ac, recomputed identically, so canonicalization of the new fields already agrees with PHP.
Expected (PHP BundleIntegrityVerifier, same bundle): status: failed with one error:
{
"rule": "history.details_pruned",
"severity": "error",
"message": "Retention cleanup pruned this run's details at 2026-11-02T21:48:14.952676Z; the bundle has no history to replay or import.",
"path": "workflow.details_state",
"context": {
"details_pruned_at": "2026-11-02T21:48:14.952676Z",
"history_complete": false
}
}Acceptance
verify_bundlereportshistory.details_prunedas an error whenworkflow.details_state == "pruned", with the same rule id, pathworkflow.details_state, anddetails_pruned_at/history_completein the context. The verdict becomesfailed, matching PHP.- Bundles with
details_state: "retained", or without the key (older exports), are unchanged. - A regression test uses the bundle below, or an equivalent fixture.
- Python replay-verify / batch simulation surfaces the failure, and does not report the bundle as replay-ready.
Older pruned exports, produced before workflow#980, carry no marker and can't be detected from the bundle alone. That is out of scope.
pruned-bundle.json (PHP Core #980 head 2e562997, test data only)
{
"schema": "durable-workflow.v2.history-export",
"schema_version": 2,
"exported_at": "2026-11-02T21:48:14.974261Z",
"dedupe_key": "01m6f3qvzpvz4jth4ag0aczfv8:6:2026-11-02T21:48:14.702328Z:details-pruned:2026-11-02T21:48:14.952676Z",
"history_complete": false,
"workflow": {
"instance_id": "pruned-parity-repro",
"run_id": "01m6f3qvzpvz4jth4ag0aczfv8",
"run_number": 1,
"is_current_run": true,
"current_run_id": "01m6f3qvzpvz4jth4ag0aczfv8",
"current_run_source": "run_order_fallback",
"workflow_type": "test-greeting-workflow",
"workflow_class": "Tests\\Fixtures\\V2\\TestGreetingWorkflow",
"source_runtime": "embedded",
"source_process_class": "application_process",
"business_key": null,
"visibility_labels": [],
"memo": [],
"memo_payload": {
"codec": "avro",
"blob": "wwHioz3/VYAiNw4A"
},
"search_attributes": [],
"status": "completed",
"status_bucket": "completed",
"closed_reason": "completed",
"archived_at": null,
"archive_command_id": null,
"archive_reason": null,
"compatibility": null,
"connection": null,
"queue": "default",
"last_history_sequence": 6,
"started_at": "2026-11-02T21:48:10.415185Z",
"closed_at": "2026-11-02T21:48:14.696276Z",
"last_progress_at": "2026-11-02T21:48:14.702328Z",
"details_state": "pruned",
"details_pruned_at": "2026-11-02T21:48:14.952676Z"
},
"payloads": {
"codec": "avro",
"arguments": {
"available": true,
"data": "wwHioz3/VYAiNwwCCgZBZGEA"
},
"output": {
"available": true,
"codec": "avro",
"data": "wwHioz3/VYAiNw4GEGdyZWV0aW5nChZIZWxsbywgQWRhIRZ3b3JrZmxvd19pZAomcHJ1bmVkLXBhcml0eS1yZXBybwxydW5faWQKNDAxbTZmM3F2enB2ejRqdGg0YWcwYWN6ZnY4AA=="
}
},
"summary": null,
"selected_run": {
"waits_projection_source": "workflow_run_waits",
"timeline_projection_source": "workflow_run_timeline_entries",
"timers_projection_source": "workflow_run_timer_entries",
"timers_projection_rebuild_reasons": [],
"lineage_projection_source": "workflow_run_lineage_entries"
},
"history_events": [],
"waits": [],
"timeline": [],
"linked_intakes_scope": "selected_run",
"linked_intakes": [],
"commands": [],
"signals": [],
"updates": [],
"tasks": [],
"activities": [],
"timers": [],
"failures": [],
"links": {
"projection_source": "workflow_run_lineage_entries",
"parents": [],
"children": []
},
"redaction": {
"applied": false,
"policy": null,
"paths": []
},
"codec_schemas": {
"avro": {
"encoding": "base64-avro-single-object",
"framing": "single_object",
"magic_hex": "c301",
"current_fingerprint": "e2a33dff55802237",
"writer_schemas": {
"e2a33dff55802237": {
"schema": "{\"type\":\"record\",\"name\":\"Value\",\"namespace\":\"durable_workflow.protocol\",\"fields\":[{\"name\":\"value\",\"type\":[\"null\",{\"type\":\"record\",\"name\":\"BooleanValue\",\"fields\":[{\"name\":\"boolean\",\"type\":\"boolean\"}]},{\"type\":\"record\",\"name\":\"LongValue\",\"fields\":[{\"name\":\"long\",\"type\":\"long\"}]},{\"type\":\"record\",\"name\":\"DoubleValue\",\"fields\":[{\"name\":\"double\",\"type\":\"double\"}]},{\"type\":\"record\",\"name\":\"BytesValue\",\"fields\":[{\"name\":\"bytes\",\"type\":\"bytes\"}]},{\"type\":\"record\",\"name\":\"StringValue\",\"fields\":[{\"name\":\"string\",\"type\":\"string\"}]},{\"type\":\"record\",\"name\":\"ArrayValue\",\"fields\":[{\"name\":\"items\",\"type\":{\"type\":\"array\",\"items\":\"Value\"}}]},{\"type\":\"record\",\"name\":\"MapValue\",\"fields\":[{\"name\":\"entries\",\"type\":{\"type\":\"map\",\"values\":\"Value\"}}]}]}]}",
"fingerprint": "crc64-avro:e2a33dff55802237"
}
}
}
},
"payload_manifest": {
"version": 1,
"entries": [
{
"path": "workflow.memo_payload",
"codec": "avro",
"available": true,
"redacted": false,
"encoding": "base64-avro-single-object",
"avro_framing": "single_object",
"avro_prefix_hex": "c301",
"writer_schema": "{\"type\":\"record\",\"name\":\"Value\",\"namespace\":\"durable_workflow.protocol\",\"fields\":[{\"name\":\"value\",\"type\":[\"null\",{\"type\":\"record\",\"name\":\"BooleanValue\",\"fields\":[{\"name\":\"boolean\",\"type\":\"boolean\"}]},{\"type\":\"record\",\"name\":\"LongValue\",\"fields\":[{\"name\":\"long\",\"type\":\"long\"}]},{\"type\":\"record\",\"name\":\"DoubleValue\",\"fields\":[{\"name\":\"double\",\"type\":\"double\"}]},{\"type\":\"record\",\"name\":\"BytesValue\",\"fields\":[{\"name\":\"bytes\",\"type\":\"bytes\"}]},{\"type\":\"record\",\"name\":\"StringValue\",\"fields\":[{\"name\":\"string\",\"type\":\"string\"}]},{\"type\":\"record\",\"name\":\"ArrayValue\",\"fields\":[{\"name\":\"items\",\"type\":{\"type\":\"array\",\"items\":\"Value\"}}]},{\"type\":\"record\",\"name\":\"MapValue\",\"fields\":[{\"name\":\"entries\",\"type\":{\"type\":\"map\",\"values\":\"Value\"}}]}]}]}",
"writer_schema_fingerprint": "crc64-avro:e2a33dff55802237",
"diagnostic": null
},
{
"path": "payloads.arguments.data",
"codec": "avro",
"available": true,
"redacted": false,
"encoding": "base64-avro-single-object",
"avro_framing": "single_object",
"avro_prefix_hex": "c301",
"writer_schema": "{\"type\":\"record\",\"name\":\"Value\",\"namespace\":\"durable_workflow.protocol\",\"fields\":[{\"name\":\"value\",\"type\":[\"null\",{\"type\":\"record\",\"name\":\"BooleanValue\",\"fields\":[{\"name\":\"boolean\",\"type\":\"boolean\"}]},{\"type\":\"record\",\"name\":\"LongValue\",\"fields\":[{\"name\":\"long\",\"type\":\"long\"}]},{\"type\":\"record\",\"name\":\"DoubleValue\",\"fields\":[{\"name\":\"double\",\"type\":\"double\"}]},{\"type\":\"record\",\"name\":\"BytesValue\",\"fields\":[{\"name\":\"bytes\",\"type\":\"bytes\"}]},{\"type\":\"record\",\"name\":\"StringValue\",\"fields\":[{\"name\":\"string\",\"type\":\"string\"}]},{\"type\":\"record\",\"name\":\"ArrayValue\",\"fields\":[{\"name\":\"items\",\"type\":{\"type\":\"array\",\"items\":\"Value\"}}]},{\"type\":\"record\",\"name\":\"MapValue\",\"fields\":[{\"name\":\"entries\",\"type\":{\"type\":\"map\",\"values\":\"Value\"}}]}]}]}",
"writer_schema_fingerprint": "crc64-avro:e2a33dff55802237",
"diagnostic": null
},
{
"path": "payloads.output.data",
"codec": "avro",
"available": true,
"redacted": false,
"encoding": "base64-avro-single-object",
"avro_framing": "single_object",
"avro_prefix_hex": "c301",
"writer_schema": "{\"type\":\"record\",\"name\":\"Value\",\"namespace\":\"durable_workflow.protocol\",\"fields\":[{\"name\":\"value\",\"type\":[\"null\",{\"type\":\"record\",\"name\":\"BooleanValue\",\"fields\":[{\"name\":\"boolean\",\"type\":\"boolean\"}]},{\"type\":\"record\",\"name\":\"LongValue\",\"fields\":[{\"name\":\"long\",\"type\":\"long\"}]},{\"type\":\"record\",\"name\":\"DoubleValue\",\"fields\":[{\"name\":\"double\",\"type\":\"double\"}]},{\"type\":\"record\",\"name\":\"BytesValue\",\"fields\":[{\"name\":\"bytes\",\"type\":\"bytes\"}]},{\"type\":\"record\",\"name\":\"StringValue\",\"fields\":[{\"name\":\"string\",\"type\":\"string\"}]},{\"type\":\"record\",\"name\":\"ArrayValue\",\"fields\":[{\"name\":\"items\",\"type\":{\"type\":\"array\",\"items\":\"Value\"}}]},{\"type\":\"record\",\"name\":\"MapValue\",\"fields\":[{\"name\":\"entries\",\"type\":{\"type\":\"map\",\"values\":\"Value\"}}]}]}]}",
"writer_schema_fingerprint": "crc64-avro:e2a33dff55802237",
"diagnostic": null
}
]
},
"integrity": {
"canonicalization": "json-recursive-ksort-v1",
"checksum_algorithm": "sha256",
"checksum": "8fa57d07aa2835e1210f3d5a840a2d46e63b4d918e15689dd8818f9c7b1101ac",
"signature_algorithm": null,
"signature": null,
"key_id": null
}
}
Activity
rmcdaniel commentedon Nov 3, 2026
Installed acceptance: Python SDK 2.13.0 from PyPI. Result: PASS. (General, standing in while the sdk-python agent is offline)
durable-workflow==2.13.0; tag2.13.0→1a17df3d. Installed hash-pinned in a freshpython:3.12venv and run withpython -Ioutside any checkout.0730ed9b…86aa, sdist sha2565a946bc8…b880; both match PyPI.sha256:48529581…, revision923d45b3); details on Expose supported worker readiness and local liveness status #168.1a17df3d:tests/fixtures/history_bundles/retention_pruned/pruned-bundle.json, sha25657f12ffb…bbf, which matches itsprovenance.jsondurable-workflow-history-bundle-verifyexits 1 with statusfailedand exactly one finding:history.details_pruned(severityerror, pathworkflow.details_state). Findings equalexpected_php_verification. The checksum matches and the recomputed canonical checksum equals the provenance value8fa57d07…01ac.python -m durable_workflow.history_bundle_verifygives an identical report.schema2_published/bundles/completed.jsonandwaiting.json(sha256 values match their provenance)okand 0 findings.Cosmetic note, not blocking:
python -m durable_workflow.history_bundle_verifyprints a runpyRuntimeWarningbecause the package__init__already imports the module. The console script does not.Already closed by the merge. This comment is the installed-acceptance receipt.