Commit 112f22b
authored
feat(tasks): register tasks in authorization graph on create/delete (#246)
## Related work
Parent epic: [AGX1-264](https://linear.app/scale-epd/issue/AGX1-264) —
per-task FGAC. Follow-ups bundled in
[AGX1-291](https://linear.app/scale-epd/issue/AGX1-291).
This change is part of an 8-PR stack across 3 repos (4 merged, 4 open).
| Repo | PR | Purpose |
|---|---|---|
| scaleapi/scaleapi | ~~scaleapi/scaleapi#144783~~ ✅ merged | sgp-authz
— `Action.CANCEL` + `parent` on `register_resource` |
| scaleapi/scaleapi | ~~scaleapi/scaleapi#145000~~ ✅ merged | register
`FGAC_AGENTEX_AUTH_SPARK` routing flag |
| scaleapi/scaleapi | ~~scaleapi/scaleapi#145044~~ ✅ merged | add
`cancel` to SGP's `AgentexOperation` enum + role map |
| scaleapi/agentex | ~~scaleapi/agentex#353~~ ✅ merged | agentex-auth
per-account provider routing + `cancel` op |
| scaleapi/scaleapi | scaleapi/scaleapi#145521 | register the
`fgac-tasks-dual-write` per-account flag |
| scaleapi/agentex | scaleapi/agentex#358 | agentex-auth: gate
register/deregister by `fgac-<resource>-dual-write` |
| **scaleapi/scale-agentex** | **this PR** | **register tasks in the
FGAC authz graph on create/delete** |
| scaleapi/scale-agentex | #249 | per-RPC task
permission rewire (`update`/`cancel`) + 404/403 wrap |
**Merge order:** flag (scaleapi/scaleapi#145521, anytime) → gate
(scaleapi/agentex#358) → #246 → enable
`fgac-tasks-dual-write` per account → #249 (after
the `cancel` enum is live in every SGP env).
## Summary
Wires task create/delete into the FGAC authorization graph (the
egp-api-backend dual-write pattern). The generic register/deregister
plumbing already landed via #260, so this PR is
just the task call sites plus tests. Whether the calls actually write to
Spark AuthZ is gated per-account in agentex-auth (scaleapi/agentex#358)
by the `fgac-tasks-dual-write` flag (scaleapi/scaleapi#145521); this OSS
repo always calls through.
- **create:** `register_resource(task, parent=agent)` before persisting
the row. If the persist fails, a compensating `deregister_resource`
cleans up the tuple and the original error re-raises (register-first
means a register failure aborts with no row written).
- **delete:** delete the row first, then deregister best-effort —
failures are logged and swallowed, since Postgres is the source of truth
for existence and a tuple on an already-deleted row is invisible to
reads. The task id is resolved before the delete so the deregister can't
race a name lookup.
- `AgentTaskService` now takes `AuthorizationService` via DI.
No schema/migration, env var, or retry/metrics helper: those were part
of an earlier iteration, dropped now that the plumbing lives in #260 and
the gating in agentex-auth.
## Tests
`test_task_fgac_dual_write.py`: register-before-persist ordering,
compensation on persist failure, deregister-after-delete,
deregister-failure swallow, and missing-name no-op. Fixtures updated to
pass a no-op authorization service. (Integration tests run in CI; they
need Docker testcontainers locally.) Ruff + ruff-format clean.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR wires task `create` and `delete` into the FGAC authorization
graph using the dual-write pattern: `register_resource` fires before the
Postgres insert (registration failure aborts cleanly, no row written), a
compensating `deregister_resource` runs if the insert subsequently
fails, and `delete_task` deregisters best-effort after the row is gone.
- `AgentTaskService` now accepts `DAuthorizationService` via DI; all
existing callers and test fixtures are updated with a shared
`make_noop_authorization_service()` helper.
- New integration test class `TestTaskDualWrite` covers
register-before-persist ordering, compensation on persist failure,
deregister-after-delete, deregister-failure swallow, and missing-name
no-op.
- Test files also carry a bulk reformat of `assert (expr), msg` →
`assert expr, (msg)` for ruff compliance.
<details><summary><h3>Confidence Score: 5/5</h3></summary>
Safe to merge; the dual-write ordering is correct and all failure modes
(register failure, persist failure, deregister failure) are
intentionally handled with clear compensation logic.
The create/delete authz wiring follows the documented dual-write
contract. The compensation on persist failure is well-tested, and the
best-effort deregister on delete correctly swallows errors. Observations
raised (asyncio cancellation gap, test tier placement) are both
acknowledged by the AGX1-291 runbook or are non-blocking style points —
neither affects the correctness of the shipped code path.
No files require special attention.
</details>
<h3>Important Files Changed</h3>
| Filename | Overview |
|----------|----------|
| agentex/src/domain/services/task_service.py | Core FGAC dual-write
wiring: register-before-persist in create_task with compensation, and
best-effort deregister in delete_task; logic and comments are solid. |
| agentex/tests/integration/use_cases/test_task_authz_dual_write.py |
New integration tests covering register-before-persist, compensation on
persist failure, deregister-after-delete, failure swallow, and
missing-name no-op; one mock-only test is misclassified in the
integration suite. |
| agentex/tests/fixtures/services.py | Adds
make_noop_authorization_service() shared helper and threads
authorization_service into create_task_service factory; clean and
consistent. |
| agentex/tests/integration/fixtures/integration_client.py | Passes
make_noop_authorization_service() to AgentTaskService in
isolated_integration_app; minimal and correct change. |
| agentex/tests/integration/test_task_stream.py | Adds noop
authorization_service to two AgentTaskService constructions; also
reformats assert messages to ruff-preferred style. |
</details>
<details><summary><h3>Sequence Diagram</h3></summary>
```mermaid
sequenceDiagram
participant C as Caller
participant TS as AgentTaskService
participant AZ as AuthorizationService
participant DB as TaskRepository
Note over C,DB: create_task (register-before-persist)
C->>TS: create_task(agent, name, ...)
TS->>AZ: "register_resource(task, parent=agent)"
alt register fails
AZ-->>TS: raise
TS-->>C: re-raise (no row written)
end
AZ-->>TS: ok
TS->>DB: create(agent_id, task)
alt persist fails
DB-->>TS: raise
TS->>AZ: deregister_resource(task) [compensation]
TS-->>C: re-raise original error
end
DB-->>TS: task_entity
TS-->>C: task_entity
Note over C,DB: delete_task (best-effort deregister)
C->>TS: "delete_task(id | name)"
opt name provided, id unknown
TS->>DB: "get(name=name)"
DB-->>TS: task.id
end
TS->>DB: delete(id, name)
DB-->>TS: ok
TS->>AZ: deregister_resource(task_id) [best-effort]
alt deregister fails
AZ-->>TS: raise
TS->>TS: log + swallow
end
TS-->>C: void
```
</details>
<a
href="https://app.greptile.com/api/ide/cursor?prompt=Fix%20the%20following%202%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%202%0Aagentex%2Ftests%2Fintegration%2Fuse_cases%2Ftest_task_authz_dual_write.py%3A140-158%0A**Mock-only%20test%20misclassified%20as%20integration**%0A%0A%60test_create_compensates_with_deregister_when_persist_fails%60%20uses%20only%20%60Mock%28%29%60%20%2F%20%60AsyncMock%60%20%E2%80%94%20no%20%60isolated_repositories%60%2C%20no%20DB%20or%20Redis.%20Because%20it%20lives%20inside%20%60%40pytest.mark.integration%60%20%60TestTaskDualWrite%60%2C%20pytest%20includes%20it%20in%20the%20integration%20suite%2C%20requiring%20testcontainers%20to%20be%20running%20before%20it%20can%20execute.%20Moving%20it%20to%20%60agentex%2Ftests%2Funit%2Fuse_cases%2Ftest_task_fgac_dual_write.py%60%20%28or%20similar%29%20would%20let%20it%20run%20in%20the%20fast%20unit%20pass%20and%20give%20earlier%20feedback%20on%20the%20compensation%20logic.%0A%0A%23%23%23%20Issue%202%20of%202%0Aagentex%2Fsrc%2Fdomain%2Fservices%2Ftask_service.py%3A84-93%0A**%60asyncio.CancelledError%60%20bypasses%20compensation%20deregister**%0A%0AIn%20Python%203.8%2B%2C%20%60asyncio.CancelledError%60%20is%20a%20%60BaseException%60%2C%20not%20an%20%60Exception%60.%20If%20the%20incoming%20request%20is%20cancelled%20%28client%20disconnect%2C%20gateway%20timeout%29%20while%20%60task_repository.create%60%20is%20awaiting%20the%20DB%20write%2C%20the%20cancellation%20propagates%20straight%20through%20%60except%20Exception%60%20without%20triggering%20the%20compensation%20%60deregister_resource%60.%20The%20task%20is%20then%20registered%20in%20the%20authz%20graph%20but%20never%20written%20to%20Postgres.%20This%20is%20the%20same%20orphan%20scenario%20covered%20by%20the%20AGX1-291%20runbook%2C%20but%20it%20can%20also%20be%20triggered%20silently%20mid-flight%20%E2%80%94%20worth%20noting%20if%20the%20scan-by-%60creator_user_id%60%20cleanup%20is%20the%20intended%20recovery%20path.%0A%0A&pr=246&platform=github"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursorDark.svg?v=3"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursor.svg?v=3"><img
alt="Fix All in Cursor"
src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCursor.svg?v=3"
height="20"></picture></a> <a
href="https://app.greptile.com/ide/claude-code?prompt=Fix%20the%20following%202%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%202%0Aagentex%2Ftests%2Fintegration%2Fuse_cases%2Ftest_task_authz_dual_write.py%3A140-158%0A**Mock-only%20test%20misclassified%20as%20integration**%0A%0A%60test_create_compensates_with_deregister_when_persist_fails%60%20uses%20only%20%60Mock%28%29%60%20%2F%20%60AsyncMock%60%20%E2%80%94%20no%20%60isolated_repositories%60%2C%20no%20DB%20or%20Redis.%20Because%20it%20lives%20inside%20%60%40pytest.mark.integration%60%20%60TestTaskDualWrite%60%2C%20pytest%20includes%20it%20in%20the%20integration%20suite%2C%20requiring%20testcontainers%20to%20be%20running%20before%20it%20can%20execute.%20Moving%20it%20to%20%60agentex%2Ftests%2Funit%2Fuse_cases%2Ftest_task_fgac_dual_write.py%60%20%28or%20similar%29%20would%20let%20it%20run%20in%20the%20fast%20unit%20pass%20and%20give%20earlier%20feedback%20on%20the%20compensation%20logic.%0A%0A%23%23%23%20Issue%202%20of%202%0Aagentex%2Fsrc%2Fdomain%2Fservices%2Ftask_service.py%3A84-93%0A**%60asyncio.CancelledError%60%20bypasses%20compensation%20deregister**%0A%0AIn%20Python%203.8%2B%2C%20%60asyncio.CancelledError%60%20is%20a%20%60BaseException%60%2C%20not%20an%20%60Exception%60.%20If%20the%20incoming%20request%20is%20cancelled%20%28client%20disconnect%2C%20gateway%20timeout%29%20while%20%60task_repository.create%60%20is%20awaiting%20the%20DB%20write%2C%20the%20cancellation%20propagates%20straight%20through%20%60except%20Exception%60%20without%20triggering%20the%20compensation%20%60deregister_resource%60.%20The%20task%20is%20then%20registered%20in%20the%20authz%20graph%20but%20never%20written%20to%20Postgres.%20This%20is%20the%20same%20orphan%20scenario%20covered%20by%20the%20AGX1-291%20runbook%2C%20but%20it%20can%20also%20be%20triggered%20silently%20mid-flight%20%E2%80%94%20worth%20noting%20if%20the%20scan-by-%60creator_user_id%60%20cleanup%20is%20the%20intended%20recovery%20path.%0A%0A&repo=scaleapi%2Fscale-agentex&pr=246&platform=github"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaudeDark.svg?v=3"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaude.svg?v=3"><img
alt="Fix All in Claude Code"
src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInClaude.svg?v=3"
height="20"></picture></a> <a
href="https://chatgpt.com/codex/deeplink?prompt=IMPORTANT%3A%20Work%20in%20the%20repository%20%22scaleapi%2Fscale-agentex%22%20on%20the%20existing%20branch%20%22asher.fink%2Fagx1-274-task-dual-write%22.%20Checkout%20that%20branch%20%E2%80%94%20do%20NOT%20create%20a%20new%20branch%20or%20open%20a%20new%20PR.%20Push%20your%20changes%20to%20%22asher.fink%2Fagx1-274-task-dual-write%22.%0A%0AFix%20the%20following%202%20code%20review%20issues.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%202%0Aagentex%2Ftests%2Fintegration%2Fuse_cases%2Ftest_task_authz_dual_write.py%3A140-158%0A**Mock-only%20test%20misclassified%20as%20integration**%0A%0A%60test_create_compensates_with_deregister_when_persist_fails%60%20uses%20only%20%60Mock%28%29%60%20%2F%20%60AsyncMock%60%20%E2%80%94%20no%20%60isolated_repositories%60%2C%20no%20DB%20or%20Redis.%20Because%20it%20lives%20inside%20%60%40pytest.mark.integration%60%20%60TestTaskDualWrite%60%2C%20pytest%20includes%20it%20in%20the%20integration%20suite%2C%20requiring%20testcontainers%20to%20be%20running%20before%20it%20can%20execute.%20Moving%20it%20to%20%60agentex%2Ftests%2Funit%2Fuse_cases%2Ftest_task_fgac_dual_write.py%60%20%28or%20similar%29%20would%20let%20it%20run%20in%20the%20fast%20unit%20pass%20and%20give%20earlier%20feedback%20on%20the%20compensation%20logic.%0A%0A%23%23%23%20Issue%202%20of%202%0Aagentex%2Fsrc%2Fdomain%2Fservices%2Ftask_service.py%3A84-93%0A**%60asyncio.CancelledError%60%20bypasses%20compensation%20deregister**%0A%0AIn%20Python%203.8%2B%2C%20%60asyncio.CancelledError%60%20is%20a%20%60BaseException%60%2C%20not%20an%20%60Exception%60.%20If%20the%20incoming%20request%20is%20cancelled%20%28client%20disconnect%2C%20gateway%20timeout%29%20while%20%60task_repository.create%60%20is%20awaiting%20the%20DB%20write%2C%20the%20cancellation%20propagates%20straight%20through%20%60except%20Exception%60%20without%20triggering%20the%20compensation%20%60deregister_resource%60.%20The%20task%20is%20then%20registered%20in%20the%20authz%20graph%20but%20never%20written%20to%20Postgres.%20This%20is%20the%20same%20orphan%20scenario%20covered%20by%20the%20AGX1-291%20runbook%2C%20but%20it%20can%20also%20be%20triggered%20silently%20mid-flight%20%E2%80%94%20worth%20noting%20if%20the%20scan-by-%60creator_user_id%60%20cleanup%20is%20the%20intended%20recovery%20path.%0A%0A"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodexDark.svg?v=3"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=3"><img
alt="Fix All in Codex"
src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=3"
height="20"></picture></a>
<details><summary>Prompt To Fix All With AI</summary>
`````markdown
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
agentex/tests/integration/use_cases/test_task_authz_dual_write.py:140-158
**Mock-only test misclassified as integration**
`test_create_compensates_with_deregister_when_persist_fails` uses only `Mock()` / `AsyncMock` — no `isolated_repositories`, no DB or Redis. Because it lives inside `@pytest.mark.integration` `TestTaskDualWrite`, pytest includes it in the integration suite, requiring testcontainers to be running before it can execute. Moving it to `agentex/tests/unit/use_cases/test_task_fgac_dual_write.py` (or similar) would let it run in the fast unit pass and give earlier feedback on the compensation logic.
### Issue 2 of 2
agentex/src/domain/services/task_service.py:84-93
**`asyncio.CancelledError` bypasses compensation deregister**
In Python 3.8+, `asyncio.CancelledError` is a `BaseException`, not an `Exception`. If the incoming request is cancelled (client disconnect, gateway timeout) while `task_repository.create` is awaiting the DB write, the cancellation propagates straight through `except Exception` without triggering the compensation `deregister_resource`. The task is then registered in the authz graph but never written to Postgres. This is the same orphan scenario covered by the AGX1-291 runbook, but it can also be triggered silently mid-flight — worth noting if the scan-by-`creator_user_id` cleanup is the intended recovery path.
`````
</details>
<sub>Reviews (8): Last reviewed commit: ["feat(tasks): register tasks in
authoriza..."](6c869dc)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=34256125)</sub>
<!-- /greptile_comment -->1 parent b6e6004 commit 112f22b
9 files changed
Lines changed: 466 additions & 165 deletions
File tree
- agentex
- src/domain/services
- tests
- fixtures
- integration
- fixtures
- use_cases
- unit
- services
- use_cases
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
| 8 | + | |
7 | 9 | | |
8 | 10 | | |
9 | 11 | | |
| |||
14 | 16 | | |
15 | 17 | | |
16 | 18 | | |
| 19 | + | |
17 | 20 | | |
18 | 21 | | |
19 | 22 | | |
| |||
33 | 36 | | |
34 | 37 | | |
35 | 38 | | |
| 39 | + | |
36 | 40 | | |
37 | 41 | | |
38 | 42 | | |
39 | 43 | | |
40 | 44 | | |
41 | 45 | | |
| 46 | + | |
42 | 47 | | |
43 | 48 | | |
44 | 49 | | |
| |||
59 | 64 | | |
60 | 65 | | |
61 | 66 | | |
62 | | - | |
63 | | - | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | | - | |
68 | | - | |
69 | | - | |
70 | | - | |
71 | | - | |
72 | | - | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
73 | 83 | | |
74 | | - | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
75 | 94 | | |
76 | 95 | | |
77 | 96 | | |
| |||
91 | 110 | | |
92 | 111 | | |
93 | 112 | | |
94 | | - | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
95 | 116 | | |
96 | 117 | | |
97 | 118 | | |
| |||
214 | 235 | | |
215 | 236 | | |
216 | 237 | | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
217 | 253 | | |
218 | 254 | | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
219 | 267 | | |
220 | 268 | | |
221 | 269 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
6 | | - | |
| 6 | + | |
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
15 | 33 | | |
16 | 34 | | |
17 | 35 | | |
| |||
52 | 70 | | |
53 | 71 | | |
54 | 72 | | |
| 73 | + | |
55 | 74 | | |
56 | | - | |
| 75 | + | |
57 | 76 | | |
58 | 77 | | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
59 | 81 | | |
60 | 82 | | |
61 | 83 | | |
62 | 84 | | |
63 | 85 | | |
64 | 86 | | |
| 87 | + | |
65 | 88 | | |
66 | 89 | | |
67 | 90 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
22 | 22 | | |
23 | 23 | | |
24 | 24 | | |
| 25 | + | |
| 26 | + | |
25 | 27 | | |
26 | 28 | | |
27 | 29 | | |
| |||
455 | 457 | | |
456 | 458 | | |
457 | 459 | | |
| 460 | + | |
458 | 461 | | |
459 | 462 | | |
460 | 463 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
10 | 12 | | |
11 | 13 | | |
12 | 14 | | |
| |||
76 | 78 | | |
77 | 79 | | |
78 | 80 | | |
| 81 | + | |
79 | 82 | | |
80 | 83 | | |
81 | 84 | | |
| |||
103 | 106 | | |
104 | 107 | | |
105 | 108 | | |
| 109 | + | |
106 | 110 | | |
107 | 111 | | |
108 | 112 | | |
| |||
194 | 198 | | |
195 | 199 | | |
196 | 200 | | |
197 | | - | |
198 | | - | |
199 | | - | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
200 | 204 | | |
201 | 205 | | |
202 | 206 | | |
203 | 207 | | |
204 | 208 | | |
205 | | - | |
206 | | - | |
207 | | - | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
208 | 212 | | |
209 | 213 | | |
210 | 214 | | |
| |||
389 | 393 | | |
390 | 394 | | |
391 | 395 | | |
392 | | - | |
393 | | - | |
394 | | - | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
395 | 399 | | |
396 | 400 | | |
397 | 401 | | |
| |||
599 | 603 | | |
600 | 604 | | |
601 | 605 | | |
602 | | - | |
603 | | - | |
604 | | - | |
| 606 | + | |
| 607 | + | |
| 608 | + | |
605 | 609 | | |
606 | 610 | | |
Whitespace-only changes.
0 commit comments