Repository navigation
Commit cf6a5f1
authored
fix(replay): repair-transaction lifecycle — keep-alive, no-partial-emit, close-as-lifecycle, atomic publish (#1235)
* fix(replay): repair-transaction lifecycle (ADR 0012 decision 6 / #1234)
Agent-supervised re-record repair lifecycle, rebased onto #1225's
failure-isolated close teardown. Consolidated from the earlier iterative
rounds into the final teardown-commits model:
- R7 keep-alive keyed off PERSISTED transaction state (repairSessionHeld
signal), so a `replay --from` continuation without --save-script is still
held on divergence.
- Commit gated on transaction COMPLETION (saveScriptComplete/saveScriptCommitted),
never on `close` alone — no prefix is ever published.
- Single commit path: `commitRepairBeforeClose` runs before #1225's
`runSessionCloseTeardown` destructive steps; a repair-armed session skips the
teardown's ordinary writeSessionLog, non-repair keeps it. Idle-reap/shutdown
commit-on-completion or tombstone via `finalizeRepairTeardown`.
- BLOCKER fixes: reaped `replay --from` -> REPAIR_SESSION_EXPIRED; commit
failures surfaced (not swallowed) and keep the session for retry (with
healed-path reporting on success); race-safe atomic no-clobber publish;
minimal `[open, close]` arms the transaction.
Integrated with #1225: keeps runSessionCloseTeardown's failure-isolated
cleanup + preserved platform-close error; repair commit happens first so a
failed commit keeps the session addressable.
* fix(replay): make the atomic publish primitive decide the no-clobber race winner
BLOCKER 1 (coordinator re-review): after linkSync saw an existing target,
the no-clobber publish fell back to an unconditional renameSync once the
target was classified "incomplete" — two concurrent writers could both read
the SAME pre-existing partial as overwritable and both renameSync over it,
each returning success with no signal to the loser. A silent, undetectable
clobber.
publishNoClobberAtomically now makes every winner decision an atomic
primitive:
- linkSync is the only way to win outright (EEXIST iff a file is at the
target at that instant).
- On EEXIST, the existing file is grabbed via an atomic renameSync into a
private, uniquely-named quarantine path *before* it is inspected, so the
completeness check never races the shared path. A competing writer's own
grab racing ours surfaces as ENOENT, and we re-evaluate from the top
instead of trusting a stale read.
- A COMPLETE quarantined file is restored (best effort) and the publish is
refused; a genuinely partial one is discarded and the exclusive linkSync
is retried.
Every interleaving converges on exactly one winning linkSync and every other
writer observing a definitive, thrown "already exists" — never two silent
successes, never a torn file.
Adds a regression (session-script-writer.test.ts) with both writers starting
against the SAME pre-existing partial target, using a renameSync spy to force
a genuine interleaving (writer B's whole publish runs inside writer A's grab
step) instead of the existing competing-writer test's sequential
complete-vs-complete scenario, which never exercised this race.
* fix(daemon): preserve failed COMPLETE-transaction commits instead of a generic expiry
BLOCKER 2 (coordinator re-review): finalizeRepairTeardown ignored the
writer's { written: false, error } outcome, so a COMPLETE transaction whose
commit failed at idle-reap/daemon-shutdown teardown (no-clobber refusal,
bare-@ref, or a filesystem error) was silently swallowed. Daemon teardown
then deleted the session and left a generic "reaped before it was finalized"
REPAIR_SESSION_EXPIRED tombstone — losing the only record that a commit was
even attempted, let alone why it failed.
finalizeRepairTeardown now captures the writer's result. On a real commit
failure it writes a distinct commit-failure tombstone (RepairSessionTombstone
gains an optional commitFailure: { code, message }); request-router's
repairExpiredIfTombstoned surfaces that as a new REPAIR_COMMIT_FAILED error
carrying the real cause instead of folding it into REPAIR_SESSION_EXPIRED.
Adds a new AppErrorCode REPAIR_COMMIT_FAILED (kernel/errors.ts) with its own
hint, a session-store regression proving finalizeRepairTeardown preserves the
failure (and leaves the prior complete artifact untouched), and a
request-router regression proving the router translates a commit-failure
tombstone to REPAIR_COMMIT_FAILED rather than the generic expiry.
* fix(daemon): record the skipped terminal close in idle-reap/shutdown auto-commit
BLOCKER 3 (coordinator re-review): the source plan's terminal `close` is
skipped-while-armed (Fix 3), so it never lands in session.actions. The
explicit `close --save-script` path accounts for this by recording a
synthetic finalize close (commitRepairBeforeClose) before committing, but
finalizeRepairTeardown's auto-commit at idle-reap/daemon-shutdown never runs
that handler — its committed healed .ad was missing its own terminal close,
so the ADR's "self-contained, fresh-replayable artifact" requirement didn't
hold for this path even though the existing auto-commit test only checked
existence + the completeness sentinel.
finalizeRepairTeardown now calls a new recordRepairFinalizeCloseIfCommitting
before writeSessionLog, mirroring commitRepairBeforeClose's recording exactly
(same command/positionals/flags shape), but only when the transaction is
actually about to be committed (COMPLETE, not yet COMMITTED) — an aborted
transaction's write is a no-op regardless.
Strengthens the existing auto-commit test (session-replay-repair-transaction
.test.ts) to use a source plan with a real terminal close, and to parse the
committed script and assert it ends with ['open', 'click', 'close'] with no
bare @ref — not just sentinel/existence. Adds a session-store.ts unit
regression exercising finalizeRepairTeardown directly for the same
self-contained-artifact assertion.
* fix(daemon): serialize the no-clobber publish decision behind an exclusive lock
BLOCKER 1 (review follow-up): publishNoClobberAtomically's inspect/restore/
publish sequence was atomic per-step but not exclusive as a whole. Writer A
could quarantine an existing COMPLETE target, and — before A restored it —
writer B could linkSync its own COMPLETE artifact into the now-empty target
and return success, only for A's restore (renameSync, which replaces an
existing destination per POSIX) to silently stomp B's freshly published
bytes.
Wrap the whole decide-and-act sequence in an exclusive publish lock
(acquireNoClobberLock/releaseNoClobberLock, an atomic linkSync claim over a
PID-stamped lock file) so a competing writer for the same scriptPath cannot
begin its own decision until the lock holder's sequence has finished and
released it. A lock whose PID is provably dead is reclaimed immediately; a
lock held by a live process is never stolen, only waited on with a bounded
backoff before failing loudly.
Adds a deterministic regression that forces the exact reported interleaving
via a renameSync spy (mirroring the existing BLOCKER 1 test's technique) and
confirms the pre-existing COMPLETE artifact is never clobbered. Also relaxes
the older PARTIAL-race test's loser-message assertion, since a losing writer
may now fail via lock contention instead of the no-clobber-specific message,
depending on interleaving timing.
* fix(daemon): report retriable:true for a preserved repair-close failure
BLOCKER 3: buildRepairCloseFailureResponse preserves the session specifically
so the agent can retry close/close --save-script, but reported
details.retriable: false — machine-consistent recovery guidance requires
retriable: true whenever the session was kept addressable for a retry.
Extends the existing BLOCKER 2b/2c no-clobber-failure test with an assertion
on this contract.
* fix(daemon): run the repair close's platform close before committing
BLOCKER 2: commitRepairBeforeClose recorded a successful terminal `close`
and published the healed artifact BEFORE dispatchTargetedPlatformClose ran.
If the platform close then failed, the session was torn down and the
committed .ad falsely contained a successful close — contradicting the
existing failed-close lifecycle contract (a failed close is never recorded
as Closed).
For a repair-armed session, dispatch the targeted platform close first; only
on success does the commit (record + publish) proceed. On failure, return
without touching the session at all — same as the existing commit-failure
contract, so the agent can fix the cause and retry. runSessionCloseTeardown
gains a skipPlatformClose flag so the already-confirmed-successful close is
never dispatched a second time during teardown.
Adds a regression: a COMPLETE repair whose targeted platform close rejects
must not commit a healed .ad, must not record a close action, and must keep
the session addressable for retry; a subsequent successful retry then
commits cleanly with exactly one terminal close.
* fix(daemon): close the no-clobber lock's dead-writer TOCTOU with rename-CAS
Two waiters could both observe the same dead-PID publish lock and both
decide to reclaim it. If one waiter's reclaim (remove + re-acquire with
its own LIVE lock) completed inside the other's decision window, the
first waiter's stale rmSync(lockPath) deleted the SECOND waiter's live
lock by pathname (not the dead one it actually inspected), letting both
enter the exclusive publish section at once.
Reclaim is now a rename-based compare-and-swap: renameSync(lockPath,
uniquePath) is the atomic claim (only one racer's rename of a given
source ever succeeds; the loser gets ENOENT and retries). Only the
winner inspects what it actually grabbed at the private claim path — if
genuinely dead, discard it; if the claim raced with someone else's fresh
reclaim and grabbed their live lock instead, restore it untouched and
back off. The live holder's lock is never stolen.
Regression drives the exact two-reclaimer interleaving deterministically
via a readFileSync spy (writer B reclaims+re-acquires live, inside
writer A's reclaim window) and confirms it fails against the prior
rmSync-based reclaim.
* fix(daemon): surface repair-close retriable/diagnosticId/logPath at the wire top level
buildRepairCloseFailureResponse hand-rolled its response shape instead
of going through normalizeError, so it put retriable under
error.details.retriable — a location neither the router's
enrichDaemonError nor the client reads (both read the top-level
DaemonError.retriable) — and silently dropped the underlying platform/
commit error's details, diagnosticId, and logPath entirely.
Now routes through normalizeError like every other AppError ->
DaemonResponse conversion in this codebase, preserving the underlying
error's details/diagnosticId/logPath, with retriable forced true at the
top level (the session is retained specifically for retry, which must
never be contradicted by the underlying error's own classification).
Also fixes a companion gap in finalizeDaemonResponse: it rebuilds every
handler-RETURNED (non-thrown) failure response into a fresh AppError
before re-normalizing, but only carried hint/diagnosticId/logPath
through that reconstruction, not retriable/supportedOn — so even a
handler setting them correctly at the top level still lost them at this
step. Both are now carried through the same way, discovered only by
verifying the close fix through the actual router boundary as
requested.
Regression: an updated handler-level test confirms diagnosticId/
logPath/details survive a repair-close failure, and a new router-level
test (through createRequestHandler, not just the raw builder) confirms
retriable:true and the platform error's diagnosticId/logPath/details
all survive to the client. Both fail against the pre-fix code.
* fix(daemon): never re-dispatch an already-succeeded repair-close platform close
When a repair-armed close's targeted platform close SUCCEEDED but the
subsequent script commit FAILED (no-clobber refusal, a bare-@ref
failure, or an fs error), the session was correctly retained for retry
-- but nothing recorded that the platform close had already happened.
A retry (close --save-script=<other>) dispatched
dispatchTargetedPlatformClose again, so a non-idempotent backend could
fail or wedge recovery on a second close of an already-closed target.
SessionState now carries repairPlatformCloseSucceeded, set the moment
the platform close returns success. A subsequent repair close consumes
it and skips straight to the commit instead of re-dispatching; it is
cleared once the transaction's outcome (commit or abort) is settled, so
it never lingers past a single close attempt.
Regression: platform close succeeds, commit fails (no-clobber),
session is retained; a retry does not re-invoke
dispatchTargetedPlatformClose (asserted via call count) and still
commits cleanly to the retry path. Fails against the pre-fix code
(dispatch called twice).
* fix(daemon): enforce complete-artifact protection on explicit --save-script targets
The explicit --save-script=<path> publish path bypassed the no-clobber
completeness guard entirely (protectComplete only gated on the DEFAULT
healed-sibling marker), so it silently overwrote even a sentinel-marked
COMPLETE healed artifact at a caller-directed path. An explicit target is
caller-DIRECTED (which path to write to), never caller-AUTHORIZED to
destroy an unreviewed prior healed diff sitting there.
Gate protectComplete on repairArmed instead of the defaulted-path marker,
so every repair-armed publish (default sibling or explicit target alike)
refuses to clobber a COMPLETE artifact. Ordinary (non-repair) recordings
are unaffected: they never carry the completeness sentinel, so the guard
never actually engages for them.
* fix(daemon): replace PID-liveness lock reclaim with a TTL publish lease
reclaimDeadLock (grab lock away -> inspect PID -> restore if live) was
structurally race-prone: a three-writer interleaving let waiter A rename
waiter B's now-LIVE lock away (to inspect it), waiter C linkSync its own
lock into the momentarily-empty path, then A's "restore" (renameSync,
which replaces an existing destination) silently clobbered C's freshly
acquired lock -- and the pathname-based release could then remove a
successor's lock, not the caller's own.
Replace it with a TTL lease (LEASE_TTL_MS = 30s). The lock file's content
is now a unique owner token plus its own creation timestamp
(pid:random:createdAtMs); staleness is judged purely from that embedded
timestamp, never by asking the OS whether a PID is alive. A stale lease is
stolen via a single atomic renameSync(lockPath, <lockPath>.expired.<id>) --
exactly one caller can ever win that rename for a still-existing source --
and the grabbed content is always discarded outright: there is no restore
path at all. verifyOwnership re-checks the lease immediately before the
publish critical section, so a writer whose lease gets displaced
underneath it (by a stale steal decision racing a concurrent re-acquire)
is never fooled into publishing unprotected -- it safely aborts instead.
releaseLease only unlinks the lock file when its current token still
matches the caller's own, so release can never delete a successor's lock.
Regression coverage (session-script-writer.test.ts): a fresh lease is
never stolen; an expired lease is stolen and reclaimed cleanly; and the
reviewer's exact three-writer interleaving (a stale steal decision
grabbing a concurrently-re-acquired fresh lease) is driven deterministically
via spies on renameSync/linkSync, asserting exactly one holder ever enters
the critical section, no live claim is silently clobbered by a restore,
and release never deletes a successor's lock. Confirmed the new tests fail
against the old reclaimDeadLock implementation and pass with the lease.
* fix(daemon): bind the repair-close platform-close marker to request identity
repairPlatformCloseSucceeded was session-wide, not bound to WHICH close
request actually succeeded. An untargeted close performs no platform
operation (shouldDispatchPlatformClose is false with no positional
target), yet the flag was still set as though a real close had run; a
retry with a DIFFERENT identity -- a target newly added, or a changed
target -- then wrongly skipped the platform close entirely and committed
as though it had run.
Bind the marker to the request's identity: repairPlatformCloseIdentity
records the target (positionals) of the close whose platform close last
succeeded -- the only thing that changes what dispatchTargetedPlatformClose
actually does (close's other flags, shutdown and saveScript, feed the
post-teardown shutdown and the commit path respectively, never the
platform close dispatch itself). A retry only skips the platform close
when BOTH repairPlatformCloseSucceeded is true AND the identity matches;
otherwise it re-runs.
Regressions (session-replay-repair-transaction.test.ts): an
untargeted-then-targeted retry and a changed-target retry both must
re-dispatch the platform close (asserted via the dispatch mock call
count/args), not skip it. Confirmed both fail against the prior
session-wide boolean and pass with the identity-bound marker.
* fix(daemon): surface a shutdown-time repair-commit failure before client cleanup
A successful owned one-shot replay --save-script marks the transaction
COMPLETE and returns success BEFORE publication -- the actual commit is
deferred to daemon teardown (finalizeRepairTeardown), which runs inside
the daemon process's own shutdown handler and, on failure, writes a
REPAIR_COMMIT_FAILED tombstone. cleanupDaemonAfterRequest then removed
the owned ephemeral state dir REGARDLESS of that tombstone, so the caller
received success while the failure and its only recovery evidence were
deleted in the same breath.
session-store.ts exports findUnrecoveredRepairCommitFailure(sessionsDir),
scanning every session subdirectory for a non-expired tombstone carrying
commitFailure -- the client has no live SessionStore/session name to key
off of, only the owned state dir's filesystem path.
cleanupDaemonAfterRequest checks for it (after stopDaemonProcessForTakeover,
which waits for the daemon to actually exit -- by then any tombstone the
daemon's own shutdown handler would write is already on disk) before
rmSync'ing the state dir: if found, the state dir is preserved and the
response is overridden to a REPAIR_COMMIT_FAILED error instead of the raw
success. daemon-client.ts's sendToDaemon now returns cleanup's result
rather than the raw request result (restructured as a caught-and-rethrown
error rather than a `return` inside `finally`, which oxlint's
no-unsafe-finally rejects and which would also swallow a thrown request
failure).
Regression (daemon-client-lifecycle.test.ts): forces a shutdown-time
commit failure by pre-seeding the tombstone in the owned state dir before
the client's cleanup runs, and asserts the REPAIR_COMMIT_FAILED response
is surfaced and the state dir (with the tombstone) survives. Confirmed it
fails without the fix (raw success returned, state dir removed).
* fix(daemon): replace the no-clobber publish lock with refuse-on-exist
The TTL-lease/reclaim machinery only existed to auto-overwrite a partial
healed artifact while never clobbering a complete one — but a concurrent
complete-vs-complete race was already correct with a plain exclusive
linkSync (first wins, second sees EEXIST), and a leftover partial is a
degenerate state, not something to silently replace. Publish is now a
single exclusive linkSync: absent target succeeds, ANY pre-existing
target (complete or partial, default sibling or explicit --save-script
path) is refused. Removes the whole lock/lease/reclaim race class.
* docs(daemon): scope refuse-on-exist contract/comments to ordinary recording too
PR #1235 review blocker: SessionScriptWriter.write's refuse-on-exist publish
is uniform across repair-armed heals AND ordinary (non-repair) open/close
--save-script recording, but the ADR contract and several comments still
read as if only healed repair publication is refused and ordinary recording
keeps the old rename-replace overwrite. Maintainer decision is to keep the
behavior uniform and fix the docs/comments/coverage instead of re-scoping.
- session-script-writer.ts: clarify isRepairArmedWriteBlocked only gates
whether a publish is attempted, not what refuse-on-exist does once it is;
fix write()'s catch-block comment, which claimed no AppError was ever
raised on the ordinary path (now false since refuse-on-exist is uniform);
broaden publishHealedScriptAtomically's doc to state it is write()'s only
publish primitive for every target, referencing the removed
publishOverwriteAtomically and the future --force/--overwrite (#1258).
- session-action-recorder.ts / session-replay-runtime.ts / types.ts: fix
comments claiming an explicit --save-script=<path> (or the
saveScriptDefaultedHealedPath marker) is exempt from the clobber guard;
the guard is uniform regardless of path origin or repair-armed status.
- docs/adr/0012-interactive-replay.md: add a "Scope" paragraph making the
refusal explicitly uniform across repair and ordinary recording, and
extend the decision-6 acceptance-test bullet and migration-plan step 9
bullet to require ordinary-recording no-clobber coverage too.
- session-script-writer.test.ts: add the missing existing-target coverage
for the ORDINARY (non-repair, no saveScriptBoundary) path — refused with
bytes unchanged when the target exists (thrown, since ordinary writes
rethrow AppErrors rather than returning them), and confirmed to still
succeed against an absent target.
No runtime behavior change: publish is still a single uniform exclusive
linkSync for every --save-script target.1 parent 139153c commit cf6a5f1
25 files changed
Lines changed: 3247 additions & 131 deletions
File tree
- docs/adr
- src
- daemon
- __tests__
- client
- handlers
- __tests__
- server
- kernel
- replay
- utils/__tests__
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
576 | 576 | | |
577 | 577 | | |
578 | 578 | | |
579 | | - | |
580 | | - | |
581 | | - | |
582 | | - | |
| 579 | + | |
| 580 | + | |
| 581 | + | |
| 582 | + | |
| 583 | + | |
| 584 | + | |
583 | 585 | | |
584 | 586 | | |
585 | 587 | | |
| |||
791 | 793 | | |
792 | 794 | | |
793 | 795 | | |
794 | | - | |
795 | | - | |
796 | | - | |
797 | | - | |
798 | | - | |
799 | | - | |
800 | | - | |
801 | | - | |
802 | | - | |
803 | | - | |
804 | | - | |
805 | | - | |
806 | | - | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
807 | 827 | | |
808 | 828 | | |
809 | 829 | | |
| |||
924 | 944 | | |
925 | 945 | | |
926 | 946 | | |
927 | | - | |
928 | | - | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
929 | 951 | | |
930 | 952 | | |
931 | 953 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
122 | 122 | | |
123 | 123 | | |
124 | 124 | | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
0 commit comments