diff --git a/.skills/context-loss-ok b/.skills/context-loss-ok index 520557c..b618f0a 100644 --- a/.skills/context-loss-ok +++ b/.skills/context-loss-ok @@ -725,3 +725,12 @@ AGENTS.md :: duplicate :: never resolved as a path. Read [docs/CONVENTIONS.md](d AGENTS.md :: duplicate :: - **Watcher alone issues `content.fetch` (#90).** `scripts/seed_fetch.py` seeds AGENTS.md :: duplicate :: scratch streams; the live one takes `--production` and Watcher's identity, never AGENTS.md :: duplicate :: `replicator`'s. + +# ---- #140 curation: Memory protection demoted out of docs/DEPLOYMENT.md ---- +# Each read against its replacement. The memory section moved whole to +# docs/MEMORY-PROTECTION.md; these four lines pointed at it or at the table it +# left behind, so the same change that moved it retargets them. +AGENTS.md :: retarget :: - [DEPLOYMENT.md](docs/DEPLOYMENT.md) — the unit's lifecycle, its start guards, the co-core pin, the host's memory tunables +docs/DEPLOYMENT.md :: retarget :: The systemd unit's lifecycle, the guards it starts behind, the host's memory +docs/DEPLOYMENT.md :: retarget :: tunables, and the co-core pin. What it reports when it *fails* is in +docs/DEPLOYMENT.md :: retarget :: from the table above — `daemon-reload` alone silently keeps the old values, and diff --git a/.skills/context-metrics.jsonl b/.skills/context-metrics.jsonl index 89a2542..42f8157 100644 --- a/.skills/context-metrics.jsonl +++ b/.skills/context-metrics.jsonl @@ -60,3 +60,7 @@ {"actions": ["baseline:scheduled"], "budget": 6000, "bytes": 14190, "claims_dropped": null, "claims_warranted": null, "counts": null, "counts_acked": null, "delta_days": 7, "delta_tokens": -4, "docs_orphaned": 0, "docs_total": 22, "docs_unindexed": 0, "file": "AGENTS.md", "lines": 249, "links_dead": 0, "links_dead_anchors": 0, "no_loss": null, "no_loss_warrants": null, "note": null, "over_budget": true, "repo": "replicator", "repo_commit": "bd49943", "seams": 2, "seams_acked": 35, "skill_commit": "5ff7a82", "skill_version": "1.31", "tokens": 6005, "tokens_exact": true, "tokens_live": 166891, "top_section": "Bus Conventions", "top_section_share": 28, "ts": "2026-10-13"} {"actions": ["baseline:pre-curation"], "budget": 6000, "bytes": 14641, "claims_dropped": null, "claims_warranted": null, "counts": null, "counts_acked": null, "delta_days": 3, "delta_tokens": 194, "docs_orphaned": 0, "docs_total": 22, "docs_unindexed": 0, "file": "AGENTS.md", "lines": 259, "links_dead": 0, "links_dead_anchors": 0, "no_loss": null, "no_loss_warrants": null, "note": null, "over_budget": true, "repo": "replicator", "repo_commit": "931ddbf", "seams": null, "seams_acked": null, "skill_commit": "3cea77a", "skill_version": "1.31", "tokens": 6199, "tokens_exact": true, "tokens_live": 167028, "top_section": "Bus Conventions", "top_section_share": 27, "ts": "2026-10-16"} {"actions": ["prune:Bus Conventions/blob-backends (dup docs/STORAGE.md; to_thread rule kept inline)", "prune:Bus Conventions/replicate-gcs (dup docs/CONVENTIONS.md+STREAMS.md)", "prune:Bus Conventions/watcher-issues-fetch (dup docs/STREAMS.md)", "ack:seams x4", "prune:stale counts-ok entry"], "budget": 6000, "bytes": 14099, "claims_dropped": 0, "claims_warranted": 0, "counts": 0, "counts_acked": 4, "delta_days": 0, "delta_tokens": -244, "docs_orphaned": 0, "docs_total": 22, "docs_unindexed": 0, "file": "AGENTS.md", "lines": 251, "links_dead": 0, "links_dead_anchors": 0, "no_loss": "ok", "no_loss_warrants": 9, "note": null, "over_budget": false, "repo": "replicator", "repo_commit": "c95aa3f", "seams": 0, "seams_acked": 39, "skill_commit": "3cea77a", "skill_version": "1.31", "tokens": 5955, "tokens_exact": true, "tokens_live": 166784, "top_section": "Bus Conventions", "top_section_share": 24, "ts": "2026-10-16"} +{"actions": ["baseline:pre-curation"], "budget": 6000, "bytes": 14099, "claims_dropped": null, "claims_warranted": null, "counts": null, "counts_acked": null, "delta_days": 1, "delta_tokens": 0, "docs_orphaned": 0, "docs_total": 22, "docs_unindexed": 0, "file": "AGENTS.md", "lines": 251, "links_dead": 0, "links_dead_anchors": 0, "no_loss": null, "no_loss_warrants": null, "note": null, "over_budget": false, "repo": "replicator", "repo_commit": "f638266", "seams": null, "seams_acked": null, "skill_commit": "3cea77a", "skill_version": "1.31", "tokens": 5955, "tokens_exact": true, "tokens_live": 166784, "top_section": "Bus Conventions", "top_section_share": 24, "ts": "2026-10-17"} +{"actions": ["baseline:pre-curation"], "budget": 10000, "bytes": 27998, "claims_dropped": null, "claims_warranted": null, "counts": null, "counts_acked": null, "delta_days": 23, "delta_tokens": 1533, "docs_orphaned": 4, "docs_total": 21, "docs_unindexed": 10, "file": "docs/DEPLOYMENT.md", "lines": 265, "links_dead": 0, "links_dead_anchors": 0, "no_loss": null, "no_loss_warrants": null, "note": null, "over_budget": true, "repo": "replicator", "repo_commit": "f638266", "seams": null, "seams_acked": null, "skill_commit": "3cea77a", "skill_version": "1.31", "tokens": 10889, "tokens_exact": true, "tokens_live": 142762, "top_section": "Server Lifecycle", "top_section_share": 97, "ts": "2026-10-17"} +{"actions": ["index:MEMORY-PROTECTION.md", "retarget:Detail Docs DEPLOYMENT.md line"], "budget": 6000, "bytes": 14154, "claims_dropped": 0, "claims_warranted": 0, "counts": 0, "counts_acked": 4, "delta_days": 0, "delta_tokens": 36, "docs_orphaned": 0, "docs_total": 23, "docs_unindexed": 0, "file": "AGENTS.md", "lines": 252, "links_dead": 0, "links_dead_anchors": 0, "no_loss": "ok", "no_loss_warrants": 1, "note": "#140: pointer for the doc demoted out of DEPLOYMENT.md; 9 tokens headroom left", "over_budget": false, "repo": "replicator", "repo_commit": "2b1fd5b", "seams": 0, "seams_acked": 39, "skill_commit": "3cea77a", "skill_version": "1.31", "tokens": 5991, "tokens_exact": true, "tokens_live": 167285, "top_section": "Bus Conventions", "top_section_share": 24, "ts": "2026-10-17"} +{"actions": ["demote:Memory protection", "retarget:preamble", "fix:manifest StartLimitBurst"], "budget": 10000, "bytes": 19715, "claims_dropped": 0, "claims_warranted": 0, "counts": 17, "counts_acked": 0, "delta_days": 0, "delta_tokens": -3319, "docs_orphaned": 4, "docs_total": 22, "docs_unindexed": 11, "file": "docs/DEPLOYMENT.md", "lines": 137, "links_dead": 0, "links_dead_anchors": 0, "no_loss": "ok", "no_loss_warrants": 3, "note": "#140: Memory protection -> docs/MEMORY-PROTECTION.md (3449 tokens, class B)", "over_budget": false, "repo": "replicator", "repo_commit": "2b1fd5b", "seams": 0, "seams_acked": 29, "skill_commit": "3cea77a", "skill_version": "1.31", "tokens": 7570, "tokens_exact": true, "tokens_live": 143227, "top_section": "Server Lifecycle", "top_section_share": 95, "ts": "2026-10-17"} diff --git a/.skills/context-seams-ok b/.skills/context-seams-ok index 265833e..6387b77 100644 --- a/.skills/context-seams-ok +++ b/.skills/context-seams-ok @@ -478,3 +478,13 @@ docs/SKILLS.md :: | `brainstorming` | Intent and design before building; this re # Provenance heading from #131, not this run's; nothing links its anchor (grep "in-flight-count"), # so the slug is not load-bearing. Rename it with the next COMMANDS.md edit rather than widen this one. docs/COMMANDS.md :: The patching gate's in-flight count (#131) + +# --- #140 curation: Memory protection split out of DEPLOYMENT.md ---------- +# Judged one by one; each points at content that STAYED in DEPLOYMENT.md or +# is the new doc's own split note. ENVIRONMENT.md's two name the start budget +# (#134) and the checkout guard, neither of which moved. +docs/ENVIRONMENT.md :: `REPLICATOR_MAX_CONSECUTIVE_CYCLE_FAILURES` — consecutive failed cycles +docs/ENVIRONMENT.md :: `REPLICATOR_ALLOW_ANY_CHECKOUT` — bypasses the `main`-checkout guard +docs/MEMORY-PROTECTION.md :: [DEPLOYMENT.md](DEPLOYMENT.md) (#140), which keeps the unit's lifecycle and the +docs/MEMORY-PROTECTION.md :: from [DEPLOYMENT.md](DEPLOYMENT.md#server-lifecycle)'s table +.socraticodecontextartifacts.json :: "description": "Memory protection for co-replicator, split out of docs/DEPLOYMENT.md (#140). diff --git a/.skills/context-token-counts b/.skills/context-token-counts index f2f2c8b..26c44b7 100644 --- a/.skills/context-token-counts +++ b/.skills/context-token-counts @@ -3,14 +3,15 @@ # The offline estimators prefer a file's own last exact measurement to the # repo-wide figure in .skills/context-token-ratio, falling back to it for a # file never counted exactly or since drifted far from the size recorded here. -14099 5955 AGENTS.md +14154 5991 AGENTS.md 5692 2194 docs/ARCHITECTURE.md -26724 10372 docs/COMMANDS.md +26738 10378 docs/COMMANDS.md 26391 9745 docs/CONVENTIONS.md -27998 10889 docs/DEPLOYMENT.md +19715 7570 docs/DEPLOYMENT.md 25412 10014 docs/ENVIRONMENT.md 8941 3330 docs/FAILURE-NOTIFICATION.md 21824 8554 docs/INFRASTRUCTURE.md +9228 3778 docs/MEMORY-PROTECTION.md 18208 6458 docs/POLITENESS.md 26669 10059 docs/SKILLS.md 11592 4447 docs/SOCRATICODE.md diff --git a/.skills/context-token-ratio b/.skills/context-token-ratio index 6d1c159..a01a6e4 100644 --- a/.skills/context-token-ratio +++ b/.skills/context-token-ratio @@ -1 +1 @@ -2.67 +2.66 diff --git a/.skills/doc-sections b/.skills/doc-sections index be6133b..7409e37 100644 --- a/.skills/doc-sections +++ b/.skills/doc-sections @@ -16,6 +16,7 @@ AGENTS.md: Project Layout (a new package under src/), the co-core pin under Environment & Tooling (pyproject.toml), Server Lifecycle (deploy/), and the Detail Docs roster of docs/*.md docs/DEPLOYMENT.md: the co-core pin and wheelhouse story (pyproject.toml, uv.lock); the systemd unit and its guards, which README.md's Deploy section restates (deploy/) +docs/MEMORY-PROTECTION.md: OOMScoreAdjust= on both units and tailscaled, swap and vm.min_free_kbytes, the MemoryLow= slice grant, earlyoom declined (deploy/) docs/ARCHITECTURE.md: Project Layout, one line per module (src/, scripts/) docs/ENVIRONMENT.md: every REPLICATOR_* variable, its default and the reasoning behind it (src/core/config.py) docs/COMMANDS.md: every script with its flags (scripts/) — README.md's Setup, Seeding a fetch and Deploy blocks repeat some of them diff --git a/.socraticodecontextartifacts.json b/.socraticodecontextartifacts.json index c1c2ea1..90cabea 100644 --- a/.socraticodecontextartifacts.json +++ b/.socraticodecontextartifacts.json @@ -88,7 +88,12 @@ { "name": "deployment-reference", "path": "./docs/DEPLOYMENT.md", - "description": "How the systemd unit runs and the guards it starts behind; where it runs is docs/INFRASTRUCTURE.md. Every deploy situation as one table — pull or push by where the merge happened, the cp each installed file under deploy/ needs, reset-failed after start-limit-hit — why the installed units are copies, the dev-server invocation, and the start budget: StartLimitBurst=6 in two hours, sized against the worst observed cluster outage (#94). The checkout guard's verdict table (#37, #48) and the dev worker's check at the writer (#52). The co-core pin and why its patch floor is load-bearing. Memory protection: OOMScoreAdjust=-900 on both units (#92), so the worker ranks below the dev sessions sharing the VM, which sit at oom_score_adj 0 since exe-init 14fd603 and were exempt at -1000 before it (#125); tailscaled's drop-in at -950, below the worker so the network path outlasts its only consumer (#113, #112); MemoryLow= real only through the system.slice.d grant, since cgroup2 here has no memory_recursiveprot; and why earlyoom is declined twice (#112): at -1000 it could not reach the sessions, and at 0 the kernel's own order already takes them first, so it would add only an earlier kill on reclaimable cache — the cohort's reading too (archiver#285, watcher#337, power-map#588). Also: a co-core bump that makes a field required is a two-repo deploy (#28), and the dedupe keys' stream segment (#29). The variables are in docs/ENVIRONMENT.md, the OnFailure= handler in docs/FAILURE-NOTIFICATION.md, the reconnection rehearsal in docs/TESTING.md." + "description": "How the systemd unit runs and the guards it starts behind; where it runs is docs/INFRASTRUCTURE.md. Every deploy situation as one table — pull or push by where the merge happened, the cp each installed file under deploy/ needs, reset-failed after start-limit-hit — why the installed units are copies, the dev-server invocation, and the start budget: StartLimitBurst=8 in two hours, sized against the worst observed cluster outage (#94, #134). The checkout guard's verdict table (#37, #48) and the dev worker's check at the writer (#52). The co-core pin and why its patch floor is load-bearing. The host's memory tunables are docs/MEMORY-PROTECTION.md. Also: a co-core bump that makes a field required is a two-repo deploy (#28), and the dedupe keys' stream segment (#29). The variables are in docs/ENVIRONMENT.md, the OnFailure= handler in docs/FAILURE-NOTIFICATION.md, the reconnection rehearsal in docs/TESTING.md." + }, + { + "name": "memory-protection-reference", + "path": "./docs/MEMORY-PROTECTION.md", + "description": "Memory protection for co-replicator, split out of docs/DEPLOYMENT.md (#140). OOMScoreAdjust=-900 on both units (#92), so the worker ranks below the dev sessions sharing the VM, which sit at oom_score_adj 0 since exe-init 14fd603 and were exempt at -1000 before it (#125); tailscaled's drop-in at -950, below the worker so the network path outlasts its only consumer (#113, #112); 8 GiB + 4 G swap and vm.min_free_kbytes pinned in deploy/99-co-replicator-memory.conf (#99); MemoryLow= real only through the system.slice.d grant, since cgroup2 here has no memory_recursiveprot; and why earlyoom is declined twice (#112): at -1000 it could not reach the sessions, and at 0 the kernel's own order already takes them first, so it would add only an earlier kill on reclaimable cache — the cohort's reading too (archiver#285, watcher#337, power-map#588). Not a substitute for capping a SocratiCode launch (docs/COMMANDS.md)." }, { "name": "failure-notification-reference", @@ -148,7 +153,7 @@ { "name": "dependency-contract", "path": "./pyproject.toml", - "description": "Project and tooling contract. The co-core / co-core-aio / co-core-sync pins (>=0.19.6,<0.20, for the content.persist contract since #114 step 6 — cannobserv#493; 0.19.3 brought the shared content-addressed BlobStore, cannobserv#475) with their [extract], [bus,gcs] and [gcs] extras, the floor history that explains each raise, the [tool.uv] find-links wheelhouse resolution, the ruff rule set and its skills-vendor exclusion, the ty exclusion, the replicator-worker console script, pytest configuration (asyncio auto mode, the integration and gcs markers both deselected by default, timeout) and the 80% coverage gate." + "description": "Project and tooling contract. The co-core / co-core-aio / co-core-sync pins (>=0.36.0,<0.37 since #140, a no-feature catch-up whose wheel diff covers the whole import closure; 0.19.6 brought the content.persist contract, cannobserv#493, and 0.19.3 the shared content-addressed BlobStore, cannobserv#475) with their [extract], [bus,gcs] and [gcs] extras, the floor history that explains each raise, the [tool.uv] find-links wheelhouse resolution, the ruff rule set and its skills-vendor exclusion, the ty exclusion, the replicator-worker console script, pytest configuration (asyncio auto mode, the integration and gcs markers both deselected by default, timeout) and the 80% coverage gate." }, { "name": "main-checkout-guard", diff --git a/AGENTS.md b/AGENTS.md index 7f2b369..55c3393 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,7 +34,7 @@ Python ≥3.12, uv, pytest, ruff. `ty` (`uv run ty check`) is a **non-gating** t uv run --no-project --with 'google-cloud-storage>=2,<4' python scripts/sync_wheelhouse.py ``` -Auth is ADC. Pin the current minor — `>=0.19.6,<0.20` — and raise the **patch** floor with every co-core feature the code starts depending on; the ways a skew has already failed are in [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md). +Auth is ADC. Pin the current minor — `>=0.36.0,<0.37` — and raise the **patch** floor with every co-core feature the code starts depending on; the ways a skew has already failed are in [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md). ## Code Exploration Policy @@ -232,7 +232,8 @@ source — each with its rationale and ruff gate in [docs/STYLE.md](docs/STYLE.m - [POLITENESS.md](docs/POLITENESS.md) — per-host pacing: the policy stream, sleep vs park, 429/503 escalation - [CONVENTIONS.md](docs/CONVENTIONS.md) — rules common to every stream: idempotency, validation, DLQ, `claim_stale`, the `replicator:cmd:*` keys (#80) - [STORAGE.md](docs/STORAGE.md) — blob paths and modes, the populations under `REPLICATOR_BLOB_DIR`, TTL and ceilings -- [DEPLOYMENT.md](docs/DEPLOYMENT.md) — the unit's lifecycle, its start guards, the co-core pin, the host's memory tunables +- [DEPLOYMENT.md](docs/DEPLOYMENT.md) — the unit's lifecycle, its start guards, the co-core pin +- [MEMORY-PROTECTION.md](docs/MEMORY-PROTECTION.md) — the host's memory tunables - [FAILURE-NOTIFICATION.md](docs/FAILURE-NOTIFICATION.md) — the `OnFailure=` handler: `REPLICATOR_NOTIFY_*`, notifier mode, delivery scoring - [INFRASTRUCTURE.md](docs/INFRASTRUCTURE.md) — VM topology, ports, the broker, buckets either side of the test/production line - [tailscale.md](docs/reference/tailscale.md) — this node: tailnet, ACL, DNS, broker latency diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index a7c3db1..b05ce42 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -385,7 +385,7 @@ since #99 — headroom, not immunity, as broker was 8 GiB too — and runs the worker beside these sessions. On CannObserv/broker's VM an uncapped launch degraded the tailnet for 57m48s with nothing OOM-killed (broker#17, our #94). `OOMScoreAdjust=-900` on the units is the other half and is already in -place ([DEPLOYMENT.md](DEPLOYMENT.md)) — it does not remove the need for the cap. +place ([MEMORY-PROTECTION.md](MEMORY-PROTECTION.md)) — it does not remove the need for the cap. `choom -n 500` is part of it: under an `exe-init` that starts sessions at `oom_score_adj=-1000`, as this host's did until #125, a cgroup cap *stalls* a process rather than killing it. diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 3150ad8..f2ff674 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,8 +1,9 @@ # Replicator Deployment -The systemd unit's lifecycle, the guards it starts behind, the host's memory -tunables, and the co-core pin. What it reports when it *fails* is in -[FAILURE-NOTIFICATION.md](FAILURE-NOTIFICATION.md). +The systemd unit's lifecycle, the guards it starts behind, and the co-core pin. +What it reports when it *fails* is in +[FAILURE-NOTIFICATION.md](FAILURE-NOTIFICATION.md); the host's memory tunables +are in [MEMORY-PROTECTION.md](MEMORY-PROTECTION.md). `AGENTS.md` keeps the two-env-file boundary and the restart command; the reasoning behind each of them is here. The variables themselves are in [ENVIRONMENT.md](ENVIRONMENT.md), which indexes every one and @@ -60,138 +61,9 @@ notifier mode and its delivery scoring: ### Memory protection — the worker outranks the dev session that shares this VM -Both units set `OOMScoreAdjust=-900`. co-replicator is also the dev workspace: -VSCode Server, Claude Code, and any MCP server they start share the VM with the -worker. At the default adj of 0 the worker read `oom_score` 670, second from the -top of the eligible list; at -900 it reads 72, above only `tailscaled` (-950, -#112): - -```bash -cat /proc/self/oom_score_adj # 0 from a session shell; -1000 before #125 -cat /proc/$(systemctl show -p MainPID --value replicator)/oom_score # 72 at adj -900; 670 before -``` - -**Until #125 the sessions were not on that list at all.** `exe-init` 8579326 -started every session at `oom_score_adj=-1000` — ineligibility, not a low score; -28 processes held it — a bug exe.dev confirmed on 2026-09-29 -(CannObserv/status#5). `exe-init` 14fd603 starts them at 0, so the directive now -wins a real comparison. `/exe.dev/bin/` is exe.dev's to replace, so -`tests/test_deploy.py` pins the 0 live; the swap and its rollback are in #125. - -**Resized 2026-09-23 (#99): 8 GiB + 4 G swap, where the scores above were -measured at 3.9 GB with none.** The scores stand — `oom_score_adj` is a rank, -not a threshold — and the margin widened rather than closed: broker was already -8 GiB when it lost the bus for 57m48s. Swap is what makes a ceiling a -survivable reclaim instead of failed atomic allocations; it and -`vm.min_free_kbytes`, which had rescaled to only ~11 MB, are pinned in -[`deploy/99-co-replicator-memory.conf`](../deploy/99-co-replicator-memory.conf). - -**At the top of the eligible list: the user manager, `systemd --user` and -`(sd-pam)` at 733 (adj +100), then — until #113 — `tailscaled` at 670.** -Measured 2026-09-24 at 8 GiB (#112); the 675 recorded on 2026-09-18 was at -3.9 GB. Read tailscaled's place against CannObserv/broker#17, where the failure -*was* the tailnet: killing tailscaled takes the bus away exactly as effectively -as killing this worker. The same 2026-09-24 pass found a session -`dbus-daemon` above the user manager (adj +200, 800), started under -`user@1000` at 04:04. So -[`deploy/tailscaled.service.d/memory.conf`](../deploy/tailscaled.service.d/memory.conf) -gave it the worker's own -900, as CannObserv/broker#21 does — 69 and 68 after -the restart — and since #112 gives it -950, CannObserv/power-map#588's value. -At a tied adj the kernel takes whichever has the larger RSS, a coin toss here: -the worker's python read 72 (88 MiB) and `tailscaled` 71 (79 MiB) on -2026-09-29, and `tailscaled` has peaked at 123 MiB. Below the worker, the -network path outlasts its only consumer: a killed worker restarts into a -working bus path, while a killed `tailscaled` leaves the worker running and cut -off, and the `OnFailure=` alert to `notifier:9000` — sent once the restart -budget is spent, not per kill — travels the tailnet. One cost: a Tailscale SSH -session into this node (`RunSSH: true`) forks from `tailscaled` and inherits --950, so dev tooling launched from it would outlive the worker — #92's -inversion. Start such a session with `choom -n 0 -- $SHELL`; raising your own -score needs no privilege. CannObserv/watcher#309 and CannObserv/archiver#285 chose -400 because their -services do not use the tailnet; this worker does. - -**Since #125 the dev tooling follows the user manager directly.** Measured -2026-09-29 after the restart: a VSCode `MainThread` at 694 (504 MB RSS), then -three `claude` processes at 682–678, all adj 0 and all ahead of every daemon — -the order #92 wanted. It is also why #112 declined earlyoom a second time. - -CannObserv/broker#17 is what it costs when it fires, and it fires in a shape -worth recognising: launching a SocratiCode server on the broker's VM took the -bus out for **57m48s with nothing OOM-killed at all**. The kernel failed -*atomic* allocations in `tailscaled` and `ksoftirqd`, so the network path -degraded while every process stayed alive — and this worker did not reconnect on -its own, which is #94. - -**`MemoryLow=` — adopted, and real only through the slice grant (#113).** The -worker and `tailscaled` each reserve 128M, against what each cgroup is charged -(measured 2026-09-24: the worker 67 MiB at peak; tailscaled 81 MiB, 60 MiB of it -RSS, with a 123 MiB peak), and -[`deploy/system.slice.d/replicator-memory.conf`](../deploy/system.slice.d/replicator-memory.conf) -grants `system.slice` their sum, 256M. The grant is the load-bearing file: -cgroup2 here is mounted without `memory_recursiveprot`, so a unit keeps no more -`memory.low` than its slice grants, and `system.slice` defaults to 0 — without -it both reservations are reported by `systemctl show` and protect nothing. That -is what #99 and #112 called inert: inert *as configured*, not structurally. - -Under reclaim it keeps both working sets resident and moves the pressure onto -`init.scope` — the sessions, a root-level sibling holding ~4 GiB — and the -unreserved daemons. It is a reclaim priority, not a guarantee, and does nothing -for *atomic* allocations; `vm.min_free_kbytes` covers those. The grant tracks -the sum and no more, because without `memory_recursiveprot` an unclaimed grant -is never handed down; the slice is the host's, so a unit that later claims a -share here needs it added. `tests/test_deploy.py` checks the sum, and on this -host reads every claim under the slice back from `/sys/fs/cgroup`, as -`init-socraticode`'s `preflight.sh --check` does. - -What this is not: - -- **Not a substitute for capping the launch.** The cap bounds a SocratiCode - server before any killer has to act, and its `choom -n 500` keeps the capped - process killable under an `exe-init` that exempts sessions — at -1000 a cgroup - cap *stalls* a process rather than killing it. `OOMScoreAdjust=` is the - unit's half; the launch's, the capped invocation, is in [COMMANDS.md](COMMANDS.md). -- **Not `earlyoom` — declined twice (#112).** At -1000 (2026-09-24, the - packaged 1.7-2 in `--dryrun`) it could not reach the sessions: it skips -1000 - exactly as the kernel does (`kill.c`), `--prefer` or not, and would have shed - small daemons — `systemd --user` 733, `tailscaled` 670 until #113, the adj-0 - daemons 666 — while ~1.7 GiB of session tooling stayed out of reach. At 0 - (2026-09-29, after #125) it can, and adds nothing but timing: 17 MiB of RSS - sits at -1000, ~3.4 GiB is eligible, and the kernel's own order already puts - the heaviest session process first and the worker and `tailscaled` last. - The re-run `--dryrun` (1.7-2, unprivileged, thresholds forced) agrees: stock - arguments take `systemd --user` (11 MiB), as the kernel would first; tuned - with `--prefer '^(node|npm|claude|MainThread)'` and the worker, `tailscaled`, - `sshd` and `systemd` avoided, a VSCode `MainThread` (476 MiB, badness 993), - the kernel's next choice after the 14 MiB user manager. Neither setting adds - anything. On stock `-s 10` it waits for swap to be ≤10% free as well — - ~3.7 G paged out here, deep into the thrashing that ends at the kernel's own - kill. On the `-s 100,100` this - host would need, it kills at ~10% `MemAvailable`, which still counts page - cache the kernel reclaims before it kills anything — so a session spike the - kernel would have absorbed becomes a lost session. Memory PSI read 0 since boot, and none of the - three saved boots (since 2026-09-23) logged an OOM kill or an allocation - failure. The cohort read it the same way: - CannObserv/archiver#285 adopted, measured and purged it, CannObserv/watcher#337 - kept it declined, CannObserv/power-map#588 is removing it. What it would have - protected is held by scores the kernel honours and by `vm.min_free_kbytes`, - the one lever for atomic allocations. `tests/test_deploy.py::TestTheEarlyoomDecline` - fails if its config or its binary returns. Re-deciding it here would start - from `-s 100,100`: a bare `-s 100` leaves SIGKILL's swap minimum at 50%, 2 G in - use, which `vm.swappiness = 10` may never reach (gregoryfoster/skills#331). -- **Not `-1000`.** That is exemption, and an exempt worker that leaks - is unreclaimable — the kernel would work through everything else on the box - first. `-900` is the cohort's value (CannObserv/broker#25): last of the - eligible, not exempt; `tailscaled`'s -950 is below it and still eligible. - `tests/test_deploy.py` pins both bounds, for both units and tailscaled's - drop-in. - -The handler unit carries it for a sharper reason than the worker does: memory -exhaustion is one of the conditions that *fires* it, so the moment it is most -likely to run is the moment an unprotected process is most likely to be killed. - -All four files are copies under `/etc/systemd/system/`, so each needs its line -from the table above — `daemon-reload` alone silently keeps the old values, and -tailscaled keeps its old adj until it restarts. +`OOMScoreAdjust=` on both units and `tailscaled`, the host's swap and +`vm.min_free_kbytes`, the `MemoryLow=` slice grant, and why `earlyoom` is +declined: [MEMORY-PROTECTION.md](MEMORY-PROTECTION.md). ### The co-core pin, and why the patch floor is load-bearing @@ -199,9 +71,9 @@ tailscaled keeps its old adj until it restarts. mirrored into `./.wheelhouse` by `scripts/sync_wheelhouse.py` and resolved through `[tool.uv] find-links` — never from PyPI. -Auth is ADC, with one substitution: on the VM the sync uses the read-only key named by `REPLICATOR_WHEELHOUSE_CREDENTIALS` (`/etc/replicator/co-pypi-reader.json`) rather than the worker's own `GOOGLE_APPLICATION_CREDENTIALS`, which is the writer (`/etc/replicator/co-gcs-replicator-writer.json`, #29, #114); in CI it is a keyless WIF token. Pin the current minor — `>=0.19.6,<0.20`, on all three. The **patch** floor is load-bearing, not tidiness: the change-bus payloads are `extra="ignore"`, so on an older wheel a model constructed with fields it does not have yet succeeds and silently discards them. Raise the floor with every co-core feature the code starts depending on, or a version skew publishes facts that look right and carry nothing (#10). Both floors since fail *loudly* instead — a ValidationError at construction (0.8.0 requires `info_source_id` on all three fetch payloads, #19/#28) or an ImportError at load, never reaching a running worker (0.9.4 cuts the replicate contracts, #29; 0.19.3 ships the shared `BlobStore`, #114; 0.19.6 cuts the persist contract, #114 step 6). +Auth is ADC, with one substitution: on the VM the sync uses the read-only key named by `REPLICATOR_WHEELHOUSE_CREDENTIALS` (`/etc/replicator/co-pypi-reader.json`) rather than the worker's own `GOOGLE_APPLICATION_CREDENTIALS`, which is the writer (`/etc/replicator/co-gcs-replicator-writer.json`, #29, #114); in CI it is a keyless WIF token. Pin the current minor — `>=0.36.0,<0.37`, on all three. The **patch** floor is load-bearing, not tidiness: the change-bus payloads are `extra="ignore"`, so on an older wheel a model constructed with fields it does not have yet succeeds and silently discards them. Raise the floor with every co-core feature the code starts depending on, or a version skew publishes facts that look right and carry nothing (#10). Both floors since fail *loudly* instead — a ValidationError at construction (0.8.0 requires `info_source_id` on all three fetch payloads, #19/#28) or an ImportError at load, never reaching a running worker (0.9.4 cuts the replicate contracts, #29; 0.19.3 ships the shared `BlobStore`, #114; 0.19.6 cuts the persist contract, #114 step 6). -**The 0.10 → 0.13 jump (#77) is the one departure from "pin the current minor", and what justified it is evidence rather than judgement.** The hazard that rule guards is payload fields moving under `extra="ignore"`; every module this service imports proved byte-identical between the two wheels — `pure/models/changes.py`, `pure/adapters/bus/{envelope,exceptions}.py`, `effects/{bus,fetch,gcs}.py`, `pure/util/{hashing,gcs}.py`, and all of `co_core_aio` bar its `__version__`. Only `streams.py` gained anything (`group_name`, `stream_kind` — cannobserv#384), and Replicator imports no `co_v1` adapter, which is where 0.13.2's other changes landed. **Diff the wheels before any future multi-minor bump**; the version distance is not the risk, the moved surface is. Unzip both from `.wheelhouse/` and `diff -rq` them — the whole check took a minute and turned a deferred change into a safe one. +**The 0.10 → 0.13 jump (#77) was the first departure from "pin the current minor", and what justified it is evidence rather than judgement.** The hazard that rule guards is payload fields moving under `extra="ignore"`; every module this service imports proved byte-identical between the two wheels — `pure/models/changes.py`, `pure/adapters/bus/{envelope,exceptions}.py`, `effects/{bus,fetch,gcs}.py`, `pure/util/{hashing,gcs}.py`, and all of `co_core_aio` bar its `__version__`. Only `streams.py` gained anything (`group_name`, `stream_kind` — cannobserv#384), and Replicator imports no `co_v1` adapter, which is where 0.13.2's other changes landed. **Diff the wheels before any future multi-minor bump**; the version distance is not the risk, the moved surface is. Unzip both from `.wheelhouse/` and `diff -rq` them — the whole check took a minute and turned a deferred change into a safe one. **Diff the import closure, not just the import lines.** At 0.19 → 0.36 (#140), `changes.py`'s import of `extract.canonical` brought in the `pure/extract` package, which had changed (cannobserv#523), and no import line in Replicator names it. **Sync the wheelhouse before concluding a version is unavailable.** The #77 review reported 0.13.1 "not mirrored" from a local `ls` while the private index already carried it; the mirror had simply not been synced, and what silently fixed it was `replicator.service`'s own non-fatal `ExecStartPre` running `sync_wheelhouse.py` on the next restart. A local listing is a cache, not the index. diff --git a/docs/MEMORY-PROTECTION.md b/docs/MEMORY-PROTECTION.md new file mode 100644 index 0000000..0810b8c --- /dev/null +++ b/docs/MEMORY-PROTECTION.md @@ -0,0 +1,141 @@ +# Memory protection + +How the worker and `tailscaled` outrank the dev sessions that share this VM: +`OOMScoreAdjust=`, the host's swap and `vm.min_free_kbytes`, the `MemoryLow=` +slice grant, and why `earlyoom` is declined. Split out of +[DEPLOYMENT.md](DEPLOYMENT.md) (#140), which keeps the unit's lifecycle and the +`cp` each file below needs; capping a SocratiCode launch is in +[COMMANDS.md](COMMANDS.md). + +Both units set `OOMScoreAdjust=-900`. co-replicator is also the dev workspace: +VSCode Server, Claude Code, and any MCP server they start share the VM with the +worker. At the default adj of 0 the worker read `oom_score` 670, second from the +top of the eligible list; at -900 it reads 72, above only `tailscaled` (-950, +#112): + +```bash +cat /proc/self/oom_score_adj # 0 from a session shell; -1000 before #125 +cat /proc/$(systemctl show -p MainPID --value replicator)/oom_score # 72 at adj -900; 670 before +``` + +**Until #125 the sessions were not on that list at all.** `exe-init` 8579326 +started every session at `oom_score_adj=-1000` — ineligibility, not a low score; +28 processes held it — a bug exe.dev confirmed on 2026-09-29 +(CannObserv/status#5). `exe-init` 14fd603 starts them at 0, so the directive now +wins a real comparison. `/exe.dev/bin/` is exe.dev's to replace, so +`tests/test_deploy.py` pins the 0 live; the swap and its rollback are in #125. + +**Resized 2026-09-23 (#99): 8 GiB + 4 G swap, where the scores above were +measured at 3.9 GB with none.** The scores stand — `oom_score_adj` is a rank, +not a threshold — and the margin widened rather than closed: broker was already +8 GiB when it lost the bus for 57m48s. Swap is what makes a ceiling a +survivable reclaim instead of failed atomic allocations; it and +`vm.min_free_kbytes`, which had rescaled to only ~11 MB, are pinned in +[`deploy/99-co-replicator-memory.conf`](../deploy/99-co-replicator-memory.conf). + +**At the top of the eligible list: the user manager, `systemd --user` and +`(sd-pam)` at 733 (adj +100), then — until #113 — `tailscaled` at 670.** +Measured 2026-09-24 at 8 GiB (#112); the 675 recorded on 2026-09-18 was at +3.9 GB. Read tailscaled's place against CannObserv/broker#17, where the failure +*was* the tailnet: killing tailscaled takes the bus away exactly as effectively +as killing this worker. The same 2026-09-24 pass found a session +`dbus-daemon` above the user manager (adj +200, 800), started under +`user@1000` at 04:04. So +[`deploy/tailscaled.service.d/memory.conf`](../deploy/tailscaled.service.d/memory.conf) +gave it the worker's own -900, as CannObserv/broker#21 does — 69 and 68 after +the restart — and since #112 gives it -950, CannObserv/power-map#588's value. +At a tied adj the kernel takes whichever has the larger RSS, a coin toss here: +the worker's python read 72 (88 MiB) and `tailscaled` 71 (79 MiB) on +2026-09-29, and `tailscaled` has peaked at 123 MiB. Below the worker, the +network path outlasts its only consumer: a killed worker restarts into a +working bus path, while a killed `tailscaled` leaves the worker running and cut +off, and the `OnFailure=` alert to `notifier:9000` — sent once the restart +budget is spent, not per kill — travels the tailnet. One cost: a Tailscale SSH +session into this node (`RunSSH: true`) forks from `tailscaled` and inherits +-950, so dev tooling launched from it would outlive the worker — #92's +inversion. Start such a session with `choom -n 0 -- $SHELL`; raising your own +score needs no privilege. CannObserv/watcher#309 and CannObserv/archiver#285 chose -400 because their +services do not use the tailnet; this worker does. + +**Since #125 the dev tooling follows the user manager directly.** Measured +2026-09-29 after the restart: a VSCode `MainThread` at 694 (504 MB RSS), then +three `claude` processes at 682–678, all adj 0 and all ahead of every daemon — +the order #92 wanted. It is also why #112 declined earlyoom a second time. + +CannObserv/broker#17 is what it costs when it fires, and it fires in a shape +worth recognising: launching a SocratiCode server on the broker's VM took the +bus out for **57m48s with nothing OOM-killed at all**. The kernel failed +*atomic* allocations in `tailscaled` and `ksoftirqd`, so the network path +degraded while every process stayed alive — and this worker did not reconnect on +its own, which is #94. + +**`MemoryLow=` — adopted, and real only through the slice grant (#113).** The +worker and `tailscaled` each reserve 128M, against what each cgroup is charged +(measured 2026-09-24: the worker 67 MiB at peak; tailscaled 81 MiB, 60 MiB of it +RSS, with a 123 MiB peak), and +[`deploy/system.slice.d/replicator-memory.conf`](../deploy/system.slice.d/replicator-memory.conf) +grants `system.slice` their sum, 256M. The grant is the load-bearing file: +cgroup2 here is mounted without `memory_recursiveprot`, so a unit keeps no more +`memory.low` than its slice grants, and `system.slice` defaults to 0 — without +it both reservations are reported by `systemctl show` and protect nothing. That +is what #99 and #112 called inert: inert *as configured*, not structurally. + +Under reclaim it keeps both working sets resident and moves the pressure onto +`init.scope` — the sessions, a root-level sibling holding ~4 GiB — and the +unreserved daemons. It is a reclaim priority, not a guarantee, and does nothing +for *atomic* allocations; `vm.min_free_kbytes` covers those. The grant tracks +the sum and no more, because without `memory_recursiveprot` an unclaimed grant +is never handed down; the slice is the host's, so a unit that later claims a +share here needs it added. `tests/test_deploy.py` checks the sum, and on this +host reads every claim under the slice back from `/sys/fs/cgroup`, as +`init-socraticode`'s `preflight.sh --check` does. + +What this is not: + +- **Not a substitute for capping the launch.** The cap bounds a SocratiCode + server before any killer has to act, and its `choom -n 500` keeps the capped + process killable under an `exe-init` that exempts sessions — at -1000 a cgroup + cap *stalls* a process rather than killing it. `OOMScoreAdjust=` is the + unit's half; the launch's, the capped invocation, is in [COMMANDS.md](COMMANDS.md). +- **Not `earlyoom` — declined twice (#112).** At -1000 (2026-09-24, the + packaged 1.7-2 in `--dryrun`) it could not reach the sessions: it skips -1000 + exactly as the kernel does (`kill.c`), `--prefer` or not, and would have shed + small daemons — `systemd --user` 733, `tailscaled` 670 until #113, the adj-0 + daemons 666 — while ~1.7 GiB of session tooling stayed out of reach. At 0 + (2026-09-29, after #125) it can, and adds nothing but timing: 17 MiB of RSS + sits at -1000, ~3.4 GiB is eligible, and the kernel's own order already puts + the heaviest session process first and the worker and `tailscaled` last. + The re-run `--dryrun` (1.7-2, unprivileged, thresholds forced) agrees: stock + arguments take `systemd --user` (11 MiB), as the kernel would first; tuned + with `--prefer '^(node|npm|claude|MainThread)'` and the worker, `tailscaled`, + `sshd` and `systemd` avoided, a VSCode `MainThread` (476 MiB, badness 993), + the kernel's next choice after the 14 MiB user manager. Neither setting adds + anything. On stock `-s 10` it waits for swap to be ≤10% free as well — + ~3.7 G paged out here, deep into the thrashing that ends at the kernel's own + kill. On the `-s 100,100` this + host would need, it kills at ~10% `MemAvailable`, which still counts page + cache the kernel reclaims before it kills anything — so a session spike the + kernel would have absorbed becomes a lost session. Memory PSI read 0 since boot, and none of the + three saved boots (since 2026-09-23) logged an OOM kill or an allocation + failure. The cohort read it the same way: + CannObserv/archiver#285 adopted, measured and purged it, CannObserv/watcher#337 + kept it declined, CannObserv/power-map#588 is removing it. What it would have + protected is held by scores the kernel honours and by `vm.min_free_kbytes`, + the one lever for atomic allocations. `tests/test_deploy.py::TestTheEarlyoomDecline` + fails if its config or its binary returns. Re-deciding it here would start + from `-s 100,100`: a bare `-s 100` leaves SIGKILL's swap minimum at 50%, 2 G in + use, which `vm.swappiness = 10` may never reach (gregoryfoster/skills#331). +- **Not `-1000`.** That is exemption, and an exempt worker that leaks + is unreclaimable — the kernel would work through everything else on the box + first. `-900` is the cohort's value (CannObserv/broker#25): last of the + eligible, not exempt; `tailscaled`'s -950 is below it and still eligible. + `tests/test_deploy.py` pins both bounds, for both units and tailscaled's + drop-in. + +The handler unit carries it for a sharper reason than the worker does: memory +exhaustion is one of the conditions that *fires* it, so the moment it is most +likely to run is the moment an unprotected process is most likely to be killed. + +All four files are copies under `/etc/systemd/system/`, so each needs its line +from [DEPLOYMENT.md](DEPLOYMENT.md#server-lifecycle)'s table — `daemon-reload` alone silently keeps the old values, and +tailscaled keeps its old adj until it restarts. diff --git a/pyproject.toml b/pyproject.toml index 8266ca4..8cecac2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,42 @@ license = "MIT" license-files = ["LICENSE"] requires-python = ">=3.12" dependencies = [ - # Floor is 0.19.6, for the `content.persist` contract (cannobserv#493, #114 + # **Seventeen minors (0.19 -> 0.36), and the floor carries no new feature** + # (#140). The jump comes ahead of the release #136 needs, which carries + # `FetchPolicyState.generation` (cannobserv#568). Then #136 is a patch bump + # alongside its rule, rather than this diff landing with a behaviour change. + # Floor is 0.36.0, the minor's only release, and the ceiling stays at the + # minor. Both `co-core-aio` and `co-core-sync` 0.36.0 require + # `co-core>=0.36,<0.37`, so the three can't drift apart. + # + # **Diffing the wheels covered the whole import closure**, not just the + # modules named here: 56 files at 0.19.6 and 57 at 0.36.0, compared by AST + # with docstrings removed. The models and the util and bus modules are + # identical bar docstrings: `pure/models/changes.py`, `pure/util/{blobstore,gcs}.py` + # and `pure/adapters/bus/streams.py`. So no payload field moved under + # `extra="ignore"`, and `FetchPolicyState`'s fields, defaults and config are + # unchanged. The rest of `pure/adapters/bus/*`, `effects/{bus,fetch,gcs}.py`, + # `pure/util/hashing.py` and all of `co_core_aio` are byte-identical, bar + # `__version__`. Two changes are additive, and neither is called here: + # + # - `co_core_sync.drivers.blobstore.gcs` adds the pointer reconciler's verbs: + # `PointerEntry`, `list_pointer_entries` and `delete_pointer` + # (cannobserv#496). + # - `pure/extract/*` adds XML extraction and `UnsupportedExtractionError` + # (cannobserv#523). It loads only because `changes.py` imports + # `extract.canonical`. + # + # Requires-Dist and the extras are unchanged bar the self-pins. Every Breaking + # entry in the CHANGELOG across the range falls in one of three places: the + # domain models (information, information set, jurisdiction, address, + # event), their WordPress and co/v1 adapters, or `extraction_config_from_spec`. + # Replicator imports or calls none of them. That includes cannobserv#549's + # aware-only datetimes, which cover those domain models and the wp/v2 + # formatters, not the change-bus payloads. + # + # Previously: + # + # Floor was 0.19.6, for the `content.persist` contract (cannobserv#493, #114 # step 6): `ContentPersistCommand`, `BlobPersistedEvent` / `PersistFailedEvent` # and their Emit twins, and the `CONTENT_PERSIST` stream constant. On 0.19.5 the # worker fails at import. The same release makes both blob stores hash what they @@ -119,9 +154,9 @@ dependencies = [ # at module load, so a skew cannot reach a running worker at all. Additive over # 0.8.0 for everything the fetch path uses — no field on ContentFetchCommand, # BlobAvailableEvent or FetchFailedEvent moved between the two. - "co-core[extract]>=0.19.6,<0.20", - "co-core-aio[bus,gcs]>=0.19.6,<0.20", - "co-core-sync[gcs]>=0.19.6,<0.20", + "co-core[extract]>=0.36.0,<0.37", + "co-core-aio[bus,gcs]>=0.36.0,<0.37", + "co-core-sync[gcs]>=0.36.0,<0.37", "fastapi>=0.126.0,<1", "pydantic>=2.9,<3", "pydantic-settings>=2.5.0,<3", diff --git a/tests/test_deploy.py b/tests/test_deploy.py index 5e4b69a..da0fbf5 100644 --- a/tests/test_deploy.py +++ b/tests/test_deploy.py @@ -461,7 +461,7 @@ def test_the_production_units_outrank_dev_tooling_for_the_oom_killer(unit: Path) start at 0 (``TestTheSessionPremise``), so the comparison this test is named for is a real one. - Two things that rank does not buy, both recorded in docs/DEPLOYMENT.md so + Two things that rank does not buy, both recorded in docs/MEMORY-PROTECTION.md so the doc and this test say the same thing. It is no substitute for capping whatever launches a SocratiCode server — the cap bounds the launch before any killer has to act. And the worker's rank protects nothing if the kernel diff --git a/uv.lock b/uv.lock index 8f2e39f..19251c3 100644 --- a/uv.lock +++ b/uv.lock @@ -298,15 +298,15 @@ wheels = [ [[package]] name = "co-core" -version = "0.19.6" +version = "0.36.0" source = { registry = ".wheelhouse" } dependencies = [ { name = "dateparser" }, { name = "pydantic" }, ] -sdist = { path = "co_core-0.19.6.tar.gz" } +sdist = { path = "co_core-0.36.0.tar.gz" } wheels = [ - { path = "co_core-0.19.6-py3-none-any.whl" }, + { path = "co_core-0.36.0-py3-none-any.whl" }, ] [package.optional-dependencies] @@ -320,15 +320,15 @@ extract = [ [[package]] name = "co-core-aio" -version = "0.19.6" +version = "0.36.0" source = { registry = ".wheelhouse" } dependencies = [ { name = "co-core" }, { name = "httpx" }, ] -sdist = { path = "co_core_aio-0.19.6.tar.gz" } +sdist = { path = "co_core_aio-0.36.0.tar.gz" } wheels = [ - { path = "co_core_aio-0.19.6-py3-none-any.whl" }, + { path = "co_core_aio-0.36.0-py3-none-any.whl" }, ] [package.optional-dependencies] @@ -342,16 +342,16 @@ gcs = [ [[package]] name = "co-core-sync" -version = "0.19.6" +version = "0.36.0" source = { registry = ".wheelhouse" } dependencies = [ { name = "co-core" }, { name = "requests" }, { name = "urllib3" }, ] -sdist = { path = "co_core_sync-0.19.6.tar.gz" } +sdist = { path = "co_core_sync-0.36.0.tar.gz" } wheels = [ - { path = "co_core_sync-0.19.6-py3-none-any.whl" }, + { path = "co_core_sync-0.36.0-py3-none-any.whl" }, ] [package.optional-dependencies] @@ -1392,9 +1392,9 @@ dev = [ [package.metadata] requires-dist = [ - { name = "co-core", extras = ["extract"], specifier = ">=0.19.6,<0.20" }, - { name = "co-core-aio", extras = ["bus", "gcs"], specifier = ">=0.19.6,<0.20" }, - { name = "co-core-sync", extras = ["gcs"], specifier = ">=0.19.6,<0.20" }, + { name = "co-core", extras = ["extract"], specifier = ">=0.36.0,<0.37" }, + { name = "co-core-aio", extras = ["bus", "gcs"], specifier = ">=0.36.0,<0.37" }, + { name = "co-core-sync", extras = ["gcs"], specifier = ">=0.36.0,<0.37" }, { name = "fastapi", specifier = ">=0.126.0,<1" }, { name = "pydantic", specifier = ">=2.9,<3" }, { name = "pydantic-settings", specifier = ">=2.5.0,<3" },