pyattacker 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. pyattacker-0.3.3/.github/workflows/docs.yml +55 -0
  2. {pyattacker-0.3.2 → pyattacker-0.3.3}/.gitignore +2 -0
  3. {pyattacker-0.3.2 → pyattacker-0.3.3}/CHANGELOG.md +40 -0
  4. {pyattacker-0.3.2 → pyattacker-0.3.3}/PKG-INFO +15 -3
  5. {pyattacker-0.3.2 → pyattacker-0.3.3}/README.md +13 -1
  6. {pyattacker-0.3.2 → pyattacker-0.3.3}/README.zh-CN.md +12 -1
  7. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/cli.md +3 -2
  8. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/design.md +13 -6
  9. pyattacker-0.3.3/docs/documentation.md +49 -0
  10. pyattacker-0.3.3/docs/hooks.py +15 -0
  11. pyattacker-0.3.3/docs/index.md +48 -0
  12. pyattacker-0.3.3/docs/overrides/main.html +8 -0
  13. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/reference.md +59 -6
  14. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/releasing.md +3 -2
  15. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/suites.md +38 -2
  16. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/tutorial.md +11 -11
  17. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/zh-CN/cli.md +3 -2
  18. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/zh-CN/design.md +9 -5
  19. pyattacker-0.3.3/docs/zh-CN/documentation.md +48 -0
  20. pyattacker-0.3.3/docs/zh-CN/index.md +48 -0
  21. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/zh-CN/reference.md +49 -6
  22. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/zh-CN/releasing.md +3 -2
  23. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/zh-CN/suites.md +30 -1
  24. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/zh-CN/tutorial.md +11 -11
  25. pyattacker-0.3.3/mkdocs.yml +97 -0
  26. {pyattacker-0.3.2 → pyattacker-0.3.3}/pyproject.toml +13 -2
  27. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/__init__.py +1 -1
  28. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/cli.py +5 -0
  29. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/declarative.py +4 -0
  30. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/errors.py +4 -1
  31. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/export.py +6 -3
  32. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/runner.py +50 -13
  33. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/server.py +17 -6
  34. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/store/__init__.py +2 -0
  35. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/store/base.py +23 -2
  36. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/store/memory.py +3 -1
  37. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/store/sqlite.py +31 -5
  38. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/store/suite.py +268 -98
  39. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/store/writebehind.py +50 -13
  40. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/helpers.py +9 -0
  41. pyattacker-0.3.3/tests/test_admission.py +224 -0
  42. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_backends.py +1 -0
  43. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_cli.py +2 -2
  44. pyattacker-0.3.3/tests/test_dashboard_security.py +64 -0
  45. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_docs_i18n.py +2 -0
  46. pyattacker-0.3.3/tests/test_fact_batches.py +280 -0
  47. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_reported_metrics.py +2 -2
  48. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_server.py +12 -2
  49. pyattacker-0.3.3/tests/test_suite_queries.py +219 -0
  50. pyattacker-0.3.3/uv.lock +685 -0
  51. pyattacker-0.3.2/uv.lock +0 -172
  52. {pyattacker-0.3.2 → pyattacker-0.3.3}/.github/workflows/ci.yml +0 -0
  53. {pyattacker-0.3.2 → pyattacker-0.3.3}/.github/workflows/release.yml +0 -0
  54. {pyattacker-0.3.2 → pyattacker-0.3.3}/LICENSE +0 -0
  55. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/benchmark.md +0 -0
  56. {pyattacker-0.3.2 → pyattacker-0.3.3}/docs/zh-CN/benchmark.md +0 -0
  57. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/data.jsonl +0 -0
  58. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/live_metrics.py +0 -0
  59. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/llm_eval/README.md +0 -0
  60. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/llm_eval/README.zh-CN.md +0 -0
  61. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/llm_eval/__init__.py +0 -0
  62. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/llm_eval/backend.py +0 -0
  63. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/llm_eval/demo.py +0 -0
  64. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/llm_eval/pipelines.py +0 -0
  65. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/plugin_package/README.md +0 -0
  66. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/plugin_package/README.zh-CN.md +0 -0
  67. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/plugin_package/pa_demo_plugin/__init__.py +0 -0
  68. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/plugin_package/pa_demo_plugin/algorithms.py +0 -0
  69. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/plugin_package/pa_demo_plugin/codecs.py +0 -0
  70. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/plugin_package/pa_demo_plugin/tasks.py +0 -0
  71. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/plugin_package/pyproject.toml +0 -0
  72. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/plugin_tasks.yaml +0 -0
  73. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/qa_eval.yaml +0 -0
  74. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/quickstart.py +0 -0
  75. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/sharded.py +0 -0
  76. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/suites/experiment.json +0 -0
  77. {pyattacker-0.3.2 → pyattacker-0.3.3}/examples/suites/suite.json +0 -0
  78. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/__main__.py +0 -0
  79. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/algorithm.py +0 -0
  80. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/artifact.py +0 -0
  81. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/backends.py +0 -0
  82. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/benchmark/__init__.py +0 -0
  83. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/benchmark/clock.py +0 -0
  84. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/benchmark/harness.py +0 -0
  85. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/benchmark/metrics.py +0 -0
  86. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/benchmark/provider.py +0 -0
  87. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/benchmark/report.py +0 -0
  88. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/benchmark/scenario.py +0 -0
  89. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/handoff.py +0 -0
  90. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/history.py +0 -0
  91. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/merge.py +0 -0
  92. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/monitor.py +0 -0
  93. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/pipeline.py +0 -0
  94. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/plugins.py +0 -0
  95. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/reported_metrics.py +0 -0
  96. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/resource.py +0 -0
  97. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/scheduler.py +0 -0
  98. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/shard.py +0 -0
  99. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/store/visits.py +0 -0
  100. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/suite.py +0 -0
  101. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/task.py +0 -0
  102. {pyattacker-0.3.2 → pyattacker-0.3.3}/src/pyattacker/tasks/__init__.py +0 -0
  103. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_algorithms.py +0 -0
  104. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_artifact.py +0 -0
  105. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_backward.py +0 -0
  106. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_benchmark_cli.py +0 -0
  107. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_benchmark_clock.py +0 -0
  108. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_benchmark_harness.py +0 -0
  109. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_benchmark_provider.py +0 -0
  110. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_benchmark_report.py +0 -0
  111. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_benchmark_scenario.py +0 -0
  112. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_declarative.py +0 -0
  113. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_docs_examples.py +0 -0
  114. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_docs_facts.py +0 -0
  115. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_errors.py +0 -0
  116. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_export.py +0 -0
  117. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_handoff.py +0 -0
  118. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_lease_safety.py +0 -0
  119. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_m2.py +0 -0
  120. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_monitor.py +0 -0
  121. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_optional_yaml.py +0 -0
  122. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_packaging.py +0 -0
  123. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_pipeline.py +0 -0
  124. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_plugins.py +0 -0
  125. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_resume_cursor.py +0 -0
  126. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_resume_identity.py +0 -0
  127. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_retry_policy.py +0 -0
  128. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_runner.py +0 -0
  129. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_scheduler.py +0 -0
  130. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_shard.py +0 -0
  131. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_store.py +0 -0
  132. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_subprocess_lifecycle.py +0 -0
  133. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_suite.py +0 -0
  134. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_task_overrides.py +0 -0
  135. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_tasks.py +0 -0
  136. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_tutorial.py +0 -0
  137. {pyattacker-0.3.2 → pyattacker-0.3.3}/tests/test_worker_liveness.py +0 -0
@@ -0,0 +1,55 @@
1
+ name: Documentation
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: docs-${{ github.ref }}
14
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
15
+
16
+ jobs:
17
+ build:
18
+ name: Build documentation
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v7
22
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
23
+ with:
24
+ enable-cache: true
25
+ python-version: '3.11'
26
+ cache-suffix: docs
27
+ - name: Install dependencies
28
+ run: uv sync --locked --group docs
29
+ - name: Build site and check links
30
+ run: uv run --group docs mkdocs build --strict
31
+ - name: Check bilingual documentation and executable examples
32
+ run: uv run pytest tests/test_docs_i18n.py tests/test_docs_examples.py tests/test_tutorial.py
33
+ - name: Upload Pages artifact
34
+ if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
35
+ uses: actions/upload-pages-artifact@v4
36
+ with:
37
+ path: site
38
+
39
+ deploy:
40
+ name: Deploy documentation
41
+ if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
42
+ needs: build
43
+ runs-on: ubuntu-latest
44
+ permissions:
45
+ pages: write
46
+ id-token: write
47
+ environment:
48
+ name: github-pages
49
+ url: ${{ steps.deployment.outputs.page_url }}
50
+ steps:
51
+ - name: Configure Pages
52
+ uses: actions/configure-pages@v5
53
+ - name: Deploy Pages
54
+ id: deployment
55
+ uses: actions/deploy-pages@v4
@@ -15,3 +15,5 @@ runs/
15
15
  .pyattacker/
16
16
 
17
17
  .scratch/
18
+
19
+ site/
@@ -6,6 +6,45 @@ All notable changes to this project are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.3] — 2026-09-21
10
+
11
+ ### Fixed
12
+
13
+ * **Dashboard HTML injection** (#78): render event and card values as text rather than
14
+ interpolating untrusted values into HTML.
15
+ * **Atomic write-behind batches** (#79): SQLite commits attempts and events in one transaction
16
+ and assigns IDs only after commit. Failed batches remain recoverable; legacy stores stop
17
+ accepting fact writes after an uncertain failure. Suite eviction and shutdown preserve
18
+ failure boundaries and close remaining resources.
19
+ * **Suite query correctness and cost** (#80): apply experiment/run filters before handoff
20
+ limits, support task limits consistently across built-in stores, and select recent events
21
+ using timestamp indexes. Counts and grouped statistics avoid decoding full history while
22
+ preserving historical run outcomes. Read-only older databases remain supported.
23
+ * Loopback HTTP tests bypass environment proxies (#76).
24
+
25
+ ### Added
26
+
27
+ * **Bounded pipeline admission** (#81): `max_admitted` limits queued, executing and delayed
28
+ pipelines together, while due retries retain their admission. Configure it through Runner,
29
+ the run configuration or `--max-admitted`; Suite members share one Runner budget. Live
30
+ statistics and persisted run configuration expose the effective limit.
31
+ * **Bilingual documentation website** (#82): tutorials, Python API and CLI references at
32
+ <https://hazer-bjtu.github.io/pyattacker/>, with search, strict link checks and automated
33
+ GitHub Pages publishing. The site tracks main and identifies itself as development documentation.
34
+
35
+ ### Changed
36
+
37
+ * Admission is bounded by default to `4 * concurrency`; a positive `max_admitted` overrides
38
+ this. Long retry backoffs can now pause input consumption. This bounds scheduler state
39
+ counts, not payload bytes or total memory; MemoryStore still retains accumulated data.
40
+ * `tasks(limit=0)` returns an empty list; negative and non-integer task limits are rejected.
41
+ Suite timestamp indexes add disk/write and first-writer migration cost. Exact duration
42
+ percentiles still read and sort matching pipeline durations.
43
+ * English and Chinese export documentation now covers the `results` row type (#77), and
44
+ streaming-memory claims describe admission and store retention separately (#81).
45
+ * Development and documentation lockfile downloads use official PyPI URLs for portable CI
46
+ installation, without changing artifact hashes or adding library runtime dependencies (#82).
47
+
9
48
  ## [0.3.2] — 2026-09-21
10
49
 
11
50
  ### Changed
@@ -613,6 +652,7 @@ hard limit; asyncio tasks only (wrap blocking code with `asyncio.to_thread`); on
613
652
  balance is statistical; a merged report is a union, not a sum; a pipeline is a linear chain; the HTTP endpoint
614
653
  is unauthenticated.
615
654
 
655
+ [0.3.3]: https://github.com/Hazer-BJTU/pyattacker/releases/tag/v0.3.3
616
656
  [0.3.0]: https://github.com/Hazer-BJTU/pyattacker/releases/tag/v0.3.0
617
657
  [0.2.0]: https://github.com/Hazer-BJTU/pyattacker/releases/tag/v0.2.0
618
658
  [0.1.1]: https://github.com/Hazer-BJTU/pyattacker/releases/tag/v0.1.1
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyattacker
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Artifact-centric, resumable async task orchestration: pipeline / task / resource pool.
5
5
  Project-URL: Homepage, https://github.com/Hazer-BJTU/pyattacker
6
6
  Project-URL: Repository, https://github.com/Hazer-BJTU/pyattacker
7
7
  Project-URL: Issues, https://github.com/Hazer-BJTU/pyattacker/issues
8
8
  Project-URL: Changelog, https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md
9
- Project-URL: Documentation, https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/tutorial.md
9
+ Project-URL: Documentation, https://hazer-bjtu.github.io/pyattacker/
10
10
  Project-URL: Design, https://github.com/Hazer-BJTU/pyattacker/blob/main/docs/design.md
11
11
  Author: pyattacker contributors
12
12
  License: MIT
@@ -34,6 +34,8 @@ Description-Content-Type: text/markdown
34
34
 
35
35
  **English** | [简体中文](README.zh-CN.md)
36
36
 
37
+ [Documentation](https://hazer-bjtu.github.io/pyattacker/)
38
+
37
39
  > Run tens of thousands of independent tasks to completion — resumably, observably, and without
38
40
  > reimplementing endpoint pools, retries and "which rows already ran" for the fifth time.
39
41
 
@@ -119,7 +121,7 @@ async def fetch(row: dict, ctx) -> dict:
119
121
 
120
122
 
121
123
  def dataset_rows():
122
- """Your dataset. A generator works too: `map` streams it, so memory stays O(concurrency)."""
124
+ """Your dataset. `map` streams generators; max_admitted bounds unfinished pipelines."""
123
125
  return [{"q": f"question-{i}"} for i in range(4)]
124
126
 
125
127
 
@@ -135,6 +137,11 @@ with Runner(store="runs/qa.db", pools=[pool], concurrency=8) as runner:
135
137
  report.export_jsonl("runs/qa.jsonl")
136
138
  ```
137
139
 
140
+ Streaming input uses `max_admitted` (default: `4 * concurrency`) to bound queued,
141
+ running and retry-delayed pipelines together. This is a count limit, not a byte
142
+ limit; task payloads, source buffers and Store retention also consume memory.
143
+ The default `MemoryStore` retains results as data grows. See [RunConfig](docs/reference.md#runconfig).
144
+
138
145
  `ctx.acquire()` falls back to the task's declared `resource`, and `lease.report(ok=...)` feeds the pool's
139
146
  health and quota accounting.
140
147
 
@@ -524,6 +531,11 @@ every known tradeoff with its reason.
524
531
 
525
532
  ## Status
526
533
 
534
+ **0.3.3 — reliability, bounded admission and online documentation.** Atomic write-behind batches,
535
+ correct and faster Suite queries, dashboard HTML injection fixes, and a configurable
536
+ `max_admitted` limit (default: `4 * concurrency`). The bilingual documentation site is now available.
537
+ See the [release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#033--2026-09-21).
538
+
527
539
  **0.3.2 — experiment suites and supported control flow.** Independent experiments can share resource
528
540
  pools and global concurrency, with unified run/resume/report commands and optional per-experiment output
529
541
  directories. Durable run histories preserve earlier outcomes after resume. Handoffs, forward jumps,
@@ -9,6 +9,8 @@
9
9
 
10
10
  **English** | [简体中文](README.zh-CN.md)
11
11
 
12
+ [Documentation](https://hazer-bjtu.github.io/pyattacker/)
13
+
12
14
  > Run tens of thousands of independent tasks to completion — resumably, observably, and without
13
15
  > reimplementing endpoint pools, retries and "which rows already ran" for the fifth time.
14
16
 
@@ -94,7 +96,7 @@ async def fetch(row: dict, ctx) -> dict:
94
96
 
95
97
 
96
98
  def dataset_rows():
97
- """Your dataset. A generator works too: `map` streams it, so memory stays O(concurrency)."""
99
+ """Your dataset. `map` streams generators; max_admitted bounds unfinished pipelines."""
98
100
  return [{"q": f"question-{i}"} for i in range(4)]
99
101
 
100
102
 
@@ -110,6 +112,11 @@ with Runner(store="runs/qa.db", pools=[pool], concurrency=8) as runner:
110
112
  report.export_jsonl("runs/qa.jsonl")
111
113
  ```
112
114
 
115
+ Streaming input uses `max_admitted` (default: `4 * concurrency`) to bound queued,
116
+ running and retry-delayed pipelines together. This is a count limit, not a byte
117
+ limit; task payloads, source buffers and Store retention also consume memory.
118
+ The default `MemoryStore` retains results as data grows. See [RunConfig](docs/reference.md#runconfig).
119
+
113
120
  `ctx.acquire()` falls back to the task's declared `resource`, and `lease.report(ok=...)` feeds the pool's
114
121
  health and quota accounting.
115
122
 
@@ -499,6 +506,11 @@ every known tradeoff with its reason.
499
506
 
500
507
  ## Status
501
508
 
509
+ **0.3.3 — reliability, bounded admission and online documentation.** Atomic write-behind batches,
510
+ correct and faster Suite queries, dashboard HTML injection fixes, and a configurable
511
+ `max_admitted` limit (default: `4 * concurrency`). The bilingual documentation site is now available.
512
+ See the [release notes](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#033--2026-09-21).
513
+
502
514
  **0.3.2 — experiment suites and supported control flow.** Independent experiments can share resource
503
515
  pools and global concurrency, with unified run/resume/report commands and optional per-experiment output
504
516
  directories. Durable run histories preserve earlier outcomes after resume. Handoffs, forward jumps,
@@ -9,6 +9,8 @@
9
9
 
10
10
  [English](README.md) | **简体中文**
11
11
 
12
+ [在线文档](https://hazer-bjtu.github.io/pyattacker/zh-CN/)
13
+
12
14
  > 把几万条独立任务一口气跑完——崩了能续、跑着能看,而且不用第五次重写端点池、重试逻辑和"哪些行已经跑过"。
13
15
 
14
16
  ## 它解决什么问题
@@ -76,7 +78,7 @@ async def fetch(row: dict, ctx) -> dict:
76
78
 
77
79
 
78
80
  def dataset_rows():
79
- """Your dataset. A generator works too: `map` streams it, so memory stays O(concurrency)."""
81
+ """Your dataset. `map` streams generators; max_admitted bounds unfinished pipelines."""
80
82
  return [{"q": f"question-{i}"} for i in range(4)]
81
83
 
82
84
 
@@ -92,6 +94,11 @@ with Runner(store="runs/qa.db", pools=[pool], concurrency=8) as runner:
92
94
  report.export_jsonl("runs/qa.jsonl")
93
95
  ```
94
96
 
97
+ 流式输入通过 `max_admitted`(默认 `4 * concurrency`)限制排队、执行和等待重试的
98
+ 流水线总数。这是数量上限,不是字节上限;任务载荷、源缓冲和 Store 保留数据也占用内存。
99
+ 默认 `MemoryStore` 会随数据量增长保留结果。详见 [RunConfig](docs/zh-CN/reference.md#runconfig)。
100
+
101
+
95
102
  `ctx.acquire()` 不指定参数时,自动用任务声明的 `resource`。`lease.report(ok=...)` 把健康和配额信息喂回资源池。
96
103
 
97
104
  真正的网络调用写在哪?——你自己写,等框架跑通了再加:
@@ -393,6 +400,10 @@ uv run pyattacker run -c examples/qa_eval.yaml --limit 40
393
400
 
394
401
  ## 当前状态
395
402
 
403
+ **0.3.3——可靠性、准入上限与在线文档。** 提供原子 write-behind 批次、正确且更快的 Suite 查询、
404
+ 仪表盘 HTML 注入修复,以及可配置的 `max_admitted` 上限(默认 `4 * concurrency`)。
405
+ 双语文档站现已上线。详见[发布说明](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md#033--2026-09-21)。
406
+
396
407
  **0.3.2——组合实验与正式控制流特性。** 独立子实验可共享资源池和总并发,统一运行、恢复和查看结果,
397
408
  并可按实验分目录存储。持久化的运行历史在恢复后仍保留此前的结果。交接、正向跳转、回退和全量重试
398
409
  作为正式公共特性维护,通过 `control` 显式声明后启用。
@@ -46,6 +46,7 @@ pyattacker run -c config.yaml [options]
46
46
  | `--limit N` | run only the first N pipelines (a smoke test over a real dataset) |
47
47
  | `--store PATH` | override `run.store`. Used verbatim with `--shard`; otherwise a shard suffix is appended |
48
48
  | `--concurrency N` | override `run.concurrency` — attempts in flight, not pipelines alive |
49
+ | `--max-admitted N` | override `run.max_admitted`: queued + running + delayed pipelines per Runner (positive integer; default `4 * concurrency`) |
49
50
  | `--journal {full,summary}` | `full` (default) stores artifact payloads, which is what makes resume work at task granularity; `summary` keeps digests only |
50
51
  | `--label TEXT` | a label recorded on the run, for telling runs apart later |
51
52
  | `--resume` | skip finished pipelines, restart failed ones at their checkpoint |
@@ -303,7 +304,7 @@ source: { kind: jsonl, path: data.jsonl, limit: 100, key_field: id, repeats: 1 }
303
304
  ```
304
305
 
305
306
  The `run:` block accepts exactly the fields the CLI maps onto `RunConfig`: `store`, `journal`,
306
- `concurrency`, `label`, `heartbeat_s`, `grace_s`, `stale_after_s`, `strict_leases`,
307
+ `concurrency`, `max_admitted`, `label`, `heartbeat_s`, `grace_s`, `stale_after_s`, `strict_leases`,
307
308
  `stop_after_failures`, `stop_after_s`, `max_handoffs`, `retry_succeeded`, `fresh_restart`, `seed`, `notes`,
308
309
  `write_behind`,
309
310
  `write_batch`, `flush_interval`, `artifact_backend` and `meta`. Anything else is a config error, not a
@@ -397,7 +398,7 @@ Python examples, `HistoryArtifact` included.
397
398
  * [`docs/tutorial.md`](tutorial.md) — the guided path, with runnable programs
398
399
  * [`docs/reference.md`](reference.md) — every class and function the SDK exposes
399
400
  * [`docs/design.md`](design.md) — why the CLI has these commands and no others
400
- * [`README.md`](../README.md) — the compact tour
401
+ * [`README.md`](https://github.com/Hazer-BJTU/pyattacker/blob/main/README.md) — the compact tour
401
402
 
402
403
  ## Suite commands
403
404
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  **English** | [简体中文](zh-CN/design.md)
4
4
 
5
- > Version: 0.3.2 — M0–M5 complete, including the supported control flow of §4.8 (forward handoffs and
5
+ > Version: 0.3.3 — M0–M5 complete, including the supported control flow of §4.8 (forward handoffs and
6
6
  > backward traversal), enabled through explicit declarations. See "Implemented / Left for later" in Section 9.
7
7
  > In one sentence: **an async task orchestration framework centered on the artifact, using the pipeline as the unit of completion, and the resource pool as the only shared surface.**
8
8
  > It does not touch the network, does not do reduction, and does not do DAG scheduling — it is only responsible for "running tens of thousands of mutually independent pipelines to completion, reliably, recoverably, and observably".
@@ -318,6 +318,11 @@ short, and with a fake clock the pump advances virtual time instantly (so tests
318
318
  `pipelines`, `tasks`, `artifacts` — always go straight through, because a checkpoint that is not durable yet
319
319
  is not a checkpoint. A `SIGKILL` can therefore lose the last batch of history while every checkpoint stays
320
320
  intact; `--no-write-behind` trades throughput for an immediate commit per attempt.
321
+ SQLite commits a whole attempts/events batch in one transaction, publishing row IDs only after success.
322
+ A failed batch rolls back and remains buffered for retry. Suite stores batch per database, not across
323
+ all child databases. Legacy stores without atomic `write_facts` acknowledge one row at a time; after
324
+ an exception, the wrapper blocks automatic replay because that call may already have committed.
325
+ See [fact batch capability](reference.md#fact-batches-and-failure-recovery) for the recovery contract.
321
326
 
322
327
  ### 4.6 Monitoring: Operational State and Application Reports
323
328
 
@@ -655,8 +660,9 @@ digests and metadata (saves space, at the cost that intermediate artifacts canno
655
660
  rerun the whole pipeline, and it leaves a `pipeline.checkpoint_missing` event behind).
656
661
 
657
662
  **Write policy**: all store methods are synchronous — this keeps critical writes such as "record a running row
658
- before the attempt starts" from being interrupted by cancellation. Batched writes / write-behind merging is a
659
- later optimization and does not affect the interface.
663
+ before the attempt starts" from being interrupted by cancellation. Write-behind combines append-only
664
+ attempts/events through the optional atomic `write_facts` capability; state writes remain synchronous
665
+ and the required `Store` interface is unchanged.
660
666
 
661
667
  **Read policy**: the list queries above may materialize their result — a report wants a list. A whole-kind
662
668
  read that must stay bounded (an export of a large store) goes through the optional paged-iteration
@@ -710,7 +716,7 @@ pool = Pool("apis", [Resource.create("llm", capacity=4,
710
716
  template = pipeline("qa_eval", fetch | ask | judge | metrics, tags={"bench": "mmlu"})
711
717
 
712
718
  with Runner(store="runs/qa.db", pools=[pool], concurrency=64) as runner:
713
- report = runner.run(template.map(dataset_rows, repeats=3)) # a generator, streaming, memory O(concurrency)
719
+ report = runner.run(template.map(dataset_rows, repeats=3)) # streams seeds under the max_admitted budget
714
720
  print(report.summary())
715
721
  report.export_jsonl("runs/qa.jsonl")
716
722
  ```
@@ -791,7 +797,8 @@ children can never fight over one file. Each child also gets `PYATACKER_SHARD=I/
791
797
  can record its own provenance.
792
798
 
793
799
  **Row shapes** (`--rows`) for whatever consumes the results: `pipelines` (nested, the default), `tasks`,
794
- `attempts` (with the retry `decision`), `events`, `artifacts`. **Formats** (`--format`): `jsonl`, `json`, `csv`.
800
+ `attempts` (with the retry `decision`), `events`, `artifacts`, and `results` (one compact row per pipeline,
801
+ with its key, repeat, status, final payload and error; failed/interrupted rows are retained). **Formats** (`--format`): `jsonl`, `json`, `csv`.
795
802
  CSV takes its header from the first `header_rows` rows and folds anything introduced later into an `extra`
796
803
  column, which keeps memory flat without silently dropping fields. Every kind is exported in full — `events`
797
804
  used to stop at the newest 100 000 rows — and read in bounded batches; see
@@ -873,7 +880,7 @@ src/pyattacker/
873
880
  monitor.py snapshot rendering + watch
874
881
  shard.py deterministic shard assignment + per-shard store paths (M3)
875
882
  merge.py join N shard stores: de-duplicate by pipeline_id, recompute statistics (M3)
876
- export.py row shapes (pipelines/tasks/attempts/events/artifacts) and formats (jsonl/json/csv) (M3)
883
+ export.py row shapes (pipelines/tasks/attempts/events/artifacts/results) and formats (jsonl/json/csv) (M3)
877
884
  declarative.py YAML/TOML → pools + pipeline + source (including ${ENV} expansion)
878
885
  plugins.py entry-point discovery for tasks/algorithms/codecs/stores (M4)
879
886
  backends.py where artifact payloads live: inline / content-addressed files / null (M4)
@@ -0,0 +1,49 @@
1
+ # Maintaining the documentation
2
+
3
+ **English** | [简体中文](zh-CN/documentation.md)
4
+
5
+ The website uses the Markdown files in `docs/` directly. The Python API reference is currently
6
+ curated Markdown; automatic API extraction is not part of this first site migration.
7
+
8
+ ## Preview locally
9
+
10
+ From the repository root, install the locked documentation dependencies and run the server:
11
+
12
+ ```bash
13
+ uv sync --locked --group docs
14
+ uv run --group docs mkdocs serve
15
+ ```
16
+
17
+ Open the address printed by MkDocs (normally `http://127.0.0.1:8000/pyattacker/`).
18
+ The `/pyattacker/` prefix matches GitHub Pages, so relative links can be checked locally.
19
+
20
+ ## Check a change
21
+
22
+ ```bash
23
+ uv run --group docs mkdocs build --strict
24
+ uv run pytest tests/test_docs_i18n.py tests/test_docs_examples.py tests/test_tutorial.py
25
+ ```
26
+
27
+ The generated `site/` directory is ignored by Git. Keep dependencies in the `docs` dependency
28
+ group and commit `uv.lock` updates. They are not library runtime dependencies.
29
+ Add new pages to `mkdocs.yml`, with a Chinese counterpart and reciprocal language links.
30
+ Extend the document pairs in `tests/test_docs_i18n.py`; keep runnable code blocks identical
31
+ between languages. Existing tutorial/reference markers must remain so the tests execute examples.
32
+ Links to files outside `docs/` should use full GitHub URLs; Markdown pages inside `docs/` use
33
+ relative links. Strict builds check page links and anchors, but do not check external URL availability.
34
+
35
+ ## Publish
36
+
37
+ The repository's Pages source is **GitHub Actions**. The Documentation workflow builds on PRs,
38
+ pushes to `main`, and manual runs. PRs only validate; pushes to `main` or manual runs on `main`
39
+ upload the site and deploy through the `github-pages` environment. No custom domain is required.
40
+
41
+ The site is published at <https://hazer-bjtu.github.io/pyattacker/>. Check the repository's Actions
42
+ tab if a build or deployment fails. Environment protection rules may require an approval before
43
+ deployment. The deployment job alone receives Pages and OIDC write permissions.
44
+
45
+ ## Version policy
46
+
47
+ The first site tracks `main` and displays a development-version notice on every page.
48
+ It is not a frozen reference for the latest package release. A later versioned publishing setup
49
+ can add a stable release alongside development documentation.
@@ -0,0 +1,15 @@
1
+ """Preserve the repository's GitHub-style heading links in the rendered website."""
2
+
3
+
4
+ def github_slug(text: str, separator: str) -> str:
5
+ # Markdown has already stripped inline markup before calling the TOC slugger.
6
+ # Keep Unicode and repeated spaces: 'Step 1 — task' becomes 'step-1--task'.
7
+ return "".join(
8
+ char if char.isalnum() or char in "-_" else separator if char.isspace() else ""
9
+ for char in text.lower()
10
+ )
11
+
12
+
13
+ def on_config(config):
14
+ config.mdx_configs["toc"]["slugify"] = github_slug
15
+ return config
@@ -0,0 +1,48 @@
1
+ # pyattacker
2
+
3
+ **English** | [简体中文](zh-CN/index.md)
4
+
5
+ Build resumable Python workflows for model evaluations and other dataset-driven tasks.
6
+ Combine task chains, shared resource pools, retries and durable checkpoints under one Runner.
7
+
8
+ [Start the tutorial](tutorial.md){ .md-button .md-button--primary }
9
+ [Python API reference](reference.md){ .md-button }
10
+
11
+ ## Start here
12
+
13
+ Install Python 3.11 or later, then install the library:
14
+
15
+ ```bash
16
+ python -m pip install pyattacker
17
+ pyattacker demo
18
+ ```
19
+
20
+ For YAML configurations, install `pyattacker[yaml]`. The base library has no runtime dependencies.
21
+ This site follows the `main` branch; your installed release may not yet include every feature shown.
22
+ To try the development version, clone the repository and follow the [tutorial setup](tutorial.md#step-0--install-and-sanity-check).
23
+
24
+ ## Choose a path
25
+
26
+ | Your goal | Read |
27
+ | --- | --- |
28
+ | Write a task and run a dataset | [Step-by-step tutorial](tutorial.md) |
29
+ | Share endpoints, retry failures and resume work | [Resources, retry and recovery tutorial](tutorial.md#find-what-you-need) |
30
+ | Compare several experiments under one Runner | [Experiment suites](suites.md) |
31
+ | Look up Python interfaces and behavior | [API reference](reference.md) |
32
+ | Run from JSON, TOML or YAML | [CLI and configuration](cli.md) |
33
+ | Understand checkpoints and scheduling guarantees | [Architecture](design.md) |
34
+ | Compare resource scheduling strategies | [Benchmarking](benchmark.md) |
35
+
36
+ ## Know what is retained
37
+
38
+ `Runner` defaults to an in-memory store. Choose a file-backed store to retain checkpoints across
39
+ processes. `concurrency` limits executing attempts; `max_admitted` bounds queued, executing and
40
+ delayed pipelines together. Payloads, input buffers and accumulated store data also consume memory.
41
+ See [RunConfig](reference.md#runconfig) before scaling a workload.
42
+
43
+ ## Explore and contribute
44
+
45
+ Browse the [runnable examples](https://github.com/Hazer-BJTU/pyattacker/tree/main/examples),
46
+ [release history](https://github.com/Hazer-BJTU/pyattacker/blob/main/CHANGELOG.md), or
47
+ [report an issue](https://github.com/Hazer-BJTU/pyattacker/issues).
48
+ For local previews and publishing, see [Maintaining the documentation](documentation.md).
@@ -0,0 +1,8 @@
1
+ {% extends "base.html" %}
2
+ {% block announce %}
3
+ {% if page and page.file.src_uri.startswith('zh-CN/') %}
4
+ 开发版文档 · 对应 main 分支,可能包含尚未发布的功能。
5
+ {% else %}
6
+ Development documentation · Tracks main and may include unreleased features.
7
+ {% endif %}
8
+ {% endblock %}
@@ -19,7 +19,7 @@ shaped this way.
19
19
  * **Defaults matter here.** Two in particular surprise people: `Retrying(max_attempts=1)` means *no retries*
20
20
  unless you ask, and `Runner(store=":memory:")` means nothing is persisted unless you give it a path.
21
21
  * **Some examples are complete programs.** A block whose first line is `# reference/<name>.py` is written out
22
- and executed by [`tests/test_docs_examples.py`](../tests/test_docs_examples.py) on every test run — the
22
+ and executed by [`tests/test_docs_examples.py`](https://github.com/Hazer-BJTU/pyattacker/blob/main/tests/test_docs_examples.py) on every test run — the
23
23
  README and the CLI document share that contract. Blocks without a marker are fragments on purpose.
24
24
 
25
25
  ## Contents
@@ -330,7 +330,7 @@ runner.run(template.map(rows)) # one pipeline per r
330
330
  runner.run(template.map(rows, repeats=5)) # pass@5
331
331
  runner.run(template.map(rows, key_of=lambda r: r["qid"])) # ids from your dataset's primary key
332
332
 
333
- def stream(): # memory stays O(concurrency)
333
+ def stream(): # streams seeds; max_admitted bounds unfinished pipelines
334
334
  with open("dataset.jsonl") as fh:
335
335
  for line in fh:
336
336
  yield json.loads(line)
@@ -760,7 +760,27 @@ live = runner.stats() # while runnin
760
760
  print(live["in_flight_pipelines"], live["delayed_pipelines"]) # in flight vs parked in backoff
761
761
  ```
762
762
 
763
- `run()` accepts any iterator of `PipelineSpec`, so a generator keeps memory flat regardless of dataset size.
763
+ `run()` reads a `PipelineSpec` iterator only while admission capacity is available.
764
+ `max_admitted=None` (default) resolves to `4 * concurrency`; a positive integer sets
765
+ an explicit limit on queued, executing and delayed pipelines **together**. Retries
766
+ retain their admission and can return to workers while new input is paused. Lower
767
+ limits reduce memory held by unfinished work, but can leave workers idle during
768
+ backoff; a limit below concurrency also limits useful parallelism. Suite members
769
+ share one budget per Runner; shard processes each have their own budget.
770
+
771
+ This bounds the number of unfinished scheduler states, not total bytes or process
772
+ RSS. Memory also includes each retained seed/current value, source-side buffering,
773
+ worker references, write-behind buffers, caches and reporting queries. The default
774
+ `MemoryStore` retains accumulated records and artifact payloads as the run grows;
775
+ use a file-backed store and a streaming source for large datasets. A pre-built input
776
+ list or unbounded task payload remains outside the admission guarantee.
777
+
778
+ `runner.stats()` exposes `admitted_pipelines` (admitted minus terminal) and the
779
+ effective `max_admitted`; the run's persisted config records that effective limit.
780
+ Stop, cancellation and run budgets also interrupt admission waits. As before,
781
+ synchronous input iteration and blocking task code must return control before the
782
+ async scheduler can respond.
783
+
764
784
 
765
785
  ### `RunConfig`
766
786
 
@@ -771,6 +791,7 @@ Everything that shapes one run. Pass a `RunConfig`, or pass its fields as keywor
771
791
  | `store` | `":memory:"` | `":memory:"`, a SQLite path, a plugin URI, or an open store instance |
772
792
  | `journal` | `"full"` | `"full"` keeps artifact payloads (**required for task-level resume**); `"summary"` keeps only metadata |
773
793
  | `concurrency` | `16` | max attempts in flight; a pipeline parked in a retry backoff does not hold a slot |
794
+ | `max_admitted` | `None` | queued + executing + delayed pipeline limit; auto = `4 * concurrency`, otherwise a positive integer |
774
795
  | `label` | `""` | a label recorded on the run |
775
796
  | `run_id` | `None` | explicit run id; default is timestamp + digest |
776
797
  | `resume` | `False` | mark pipelines abandoned by dead runs as resumable before scheduling |
@@ -1404,6 +1425,37 @@ with closing(open_store("runs/qa.db")) as store: # reopen a finished run, a
1404
1425
  `spec` can be `":memory:"`, a path, a plugin URI (`"s3://bucket/runs.db"`), or an already-open store.
1405
1426
  Reading a live store while a run writes to it is supported — WAL allows one writer and many readers.
1406
1427
 
1428
+ ### Fact batches and failure recovery
1429
+
1430
+ `pyattacker.store.FactBatchStore` is an optional capability, separate from the required `Store`
1431
+ protocol: `write_facts(attempts, events)` atomically commits both sequences and assigns their IDs
1432
+ only after success. On exception no batch rows may be committed and input IDs must be unchanged.
1433
+ The capability owns its transaction; SQLite rejects a call inside an existing transaction without
1434
+ committing or rolling back the caller's work. An empty batch performs no transaction.
1435
+
1436
+ `WriteBehindStore` uses this capability when available. SQLite implements it, including the SQLite
1437
+ stores wrapped by either Suite layout. A failed atomic batch stays buffered and can be flushed again.
1438
+ In a split Suite, a successful child batch stays committed if a later child's batch fails; retry only
1439
+ writes the remaining batches. Eviction flushes before removing a child from the connection cache,
1440
+ so a failed flush leaves that child and its pending batch available for retry. Checkpoint, artifact,
1441
+ and task-state writes still bypass the buffer; handoff/visit transactions keep their existing boundaries.
1442
+
1443
+ A legacy store without `write_facts` is supported through individual `record_attempt`/`emit_event`
1444
+ calls. Each normally returned call removes that confirmed record from the buffer. If a call raises,
1445
+ the original exception propagates and the failing record plus its unattempted suffix stay buffered.
1446
+ Because the failing call might have committed before raising, this wrapper then refuses further
1447
+ fact writes and flush-dependent reads/flushes with `StoreUnavailable`, chaining the original error.
1448
+ `buffer_stats()["flush_blocked"]` reports this condition; `pending` includes the uncertain record.
1449
+ Reconcile persisted records with the original submitted facts before constructing another wrapper;
1450
+ do not blindly replay the pending suffix. There is no automatic exactly-once guarantee for an
1451
+ ambiguous legacy write. Custom stores that can guarantee atomic rollback should implement the optional
1452
+ capability instead. SQLite subclasses that customize fact writes must also customize `write_facts`
1453
+ or disable it with `write_facts = None` to keep using their individual write overrides.
1454
+
1455
+ Closing always releases the underlying store even when flush raises; the error remains visible.
1456
+ Suite close attempts every child, the catalog and the ownership lock, and propagates the first error.
1457
+ After a failed close, reopen/reconcile as needed; do not retry a flush on the closed connection.
1458
+
1407
1459
  ### Tables and readers
1408
1460
 
1409
1461
  | Table | One row per | Read with |
@@ -1888,6 +1940,7 @@ One row per kind, in the order `limit` truncates:
1888
1940
  | `attempts` | attempt, including each retry `decision` | `attempt_id` (write order) |
1889
1941
  | `events` | structured event | `event_id` (write order, oldest first) |
1890
1942
  | `artifacts` | artifact, intermediate ones included | pipeline order, then `seq`, then `artifact_id` |
1943
+ | `results` | pipeline result: key, repeat, state, final payload and error; failed/interrupted rows included | `created_at`, then `pipeline_id` |
1891
1944
 
1892
1945
  `limit` means the same thing for every kind — it counts rows of that kind, including `artifacts`,
1893
1946
  where it used to count pipelines:
@@ -2030,7 +2083,7 @@ print(PLUGINS.errors()) # {name: reason} for everything that failed to lo
2030
2083
  Built-ins resolve first, and a plugin that raises on import is recorded rather than propagated — so a broken
2031
2084
  plugin cannot take a run down, and it cannot fail silently either. `pyattacker plugins` prints the same
2032
2085
  information. `PluginRegistry` is the type; `PLUGINS` is the process-wide instance. A complete worked package
2033
- is in [`examples/plugin_package/`](../examples/plugin_package/README.md).
2086
+ is in [`examples/plugin_package/`](https://github.com/Hazer-BJTU/pyattacker/blob/main/examples/plugin_package/README.md).
2034
2087
 
2035
2088
  ---
2036
2089
 
@@ -2177,7 +2230,7 @@ runner.run(template.map(jsonl_source("dataset.jsonl", limit=500)))
2177
2230
 
2178
2231
  An application can publish its latest experiment values while a run is active. Pyattacker stores
2179
2232
  and displays the values; the application computes them. For a runnable accuracy example, see
2180
- [`examples/live_metrics.py`](../examples/live_metrics.py).
2233
+ [`examples/live_metrics.py`](https://github.com/Hazer-BJTU/pyattacker/blob/main/examples/live_metrics.py).
2181
2234
 
2182
2235
  ```python
2183
2236
  runner.report_metric("evaluated", completed, label="Evaluated")
@@ -2273,7 +2326,7 @@ command line.
2273
2326
  | [`docs/tutorial.md`](tutorial.md) | the guided path: seventeen runnable steps |
2274
2327
  | [`docs/cli.md`](cli.md) | commands, flags, exit codes, config file format |
2275
2328
  | [`docs/design.md`](design.md) | the model, the invariants, and the tradeoffs behind these APIs |
2276
- | [`examples/`](../examples) | complete programs, including a measured comparison of pipeline shapes |
2329
+ | [`examples/`](https://github.com/Hazer-BJTU/pyattacker/tree/main/examples) | complete programs, including a measured comparison of pipeline shapes |
2277
2330
 
2278
2331
  ## Suite API
2279
2332
 
@@ -3,7 +3,7 @@
3
3
  **English** | [简体中文](zh-CN/releasing.md)
4
4
 
5
5
  Written for maintainers with push access. Publishing is automated: you push a tag, and
6
- [`.github/workflows/release.yml`](../.github/workflows/release.yml) does the rest.
6
+ [`.github/workflows/release.yml`](https://github.com/Hazer-BJTU/pyattacker/blob/main/.github/workflows/release.yml) does the rest.
7
7
 
8
8
  ## One-time setup: trusted publishing
9
9
 
@@ -47,7 +47,8 @@ cannot be borrowed by a branch push.
47
47
  release notes. No section means the release still publishes, with a placeholder and a warning.
48
48
 
49
49
  **2. Bump the version in two places** — they must agree, and the workflow refuses the release if the tag
50
- disagrees with either:
50
+ disagrees with either. Also update the version headers in `docs/design.md` and
51
+ `docs/zh-CN/design.md`, and the current-status summaries in both READMEs:
51
52
 
52
53
  ```bash
53
54
  # pyproject.toml: version = "X.Y.Z"