mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-09-14 04:45:07 +00:00
master
68
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d71d888019 |
Merge #9698b1f9: test: eliminate shared fixture races and timing-depend…
test: eliminate shared fixture races and timing-dependent assertions nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsfdx93lyfyyuvw79qg8qe4eec4gjxkqasu3glrrsljhdgle3hw5fqtx4mfz PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Parallel tests could lose released relay ports, leave connections alive after fixture shutdown, block Git children on full pipes, or infer readiness from scheduler-dependent sleeps. This revision extends the existing relay-port PR into one consolidated reliability proposal for Grasp. - Retain SDK relay listeners through startup and exercise 32 concurrent fixtures. - Transfer reserved loopback sockets into Grasp subprocesses through a private Unix test protocol, retain the address across restart, and keep offline/recovering endpoints continuously owned. - Own HTTP, WebSocket upgrade and backend-forwarding tasks across the mock relay, auth gate, Git servers and proxy fixtures. Explicit shutdown cancels and joins requests; Git subprocess I/O drains concurrently. - Wait for event visibility and actual connected-state metrics; correct metrics URL construction and stopped-source bookkeeping. - Replace expiry scheduling margins with explicit timestamps and streaming sleeps/frame assumptions with observable gates and byte-level assertions. Tests remain enabled and parallel. Normal server binding and service configuration are unchanged; inherited listener support requires NGIT_TEST=1 and validates a listening loopback socket. The private capability probe lets the companion ngit harness retain compatibility with older releases. Targeted Linux validation in the repository development shell: 65 relay-connection tests, 33 rejected-index tests, 132 purgatory tests, all 3 streaming tests, 7 shared Git-server tests, and relay-identity/recovery tests passed. Final fixture_lifecycle passed 57 tests; the moved subprocess-restart regression passed separately. Scoped Clippy for fixture_lifecycle and relay_identity passed with warnings denied. A smoke test queried this Grasp binary using the modified ngit harness. Formatting and whitespace checks pass. These checks are scoped, not a claim of full workspace, nixpkgs sandbox or Darwin validation. Full host builds remain outstanding for the new changes. External Buzz service fixtures are outside this PR. The separate ngit reliability proposal addresses the equivalent harness and Blossom timing issues. AI assistance: implemented and reviewed with Codex (GPT-6). |
||
|
|
3a6de13568 |
Merge #edc95f2b: test(sync): simulate downtime in checkpoint integratio…
test(sync): simulate downtime in checkpoint integration test
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqswmj2l9wqv8gnmety86n59j6j2vnmdr4rvg3nhlsnadudp4v0nuggz2q9ml
PR-Author: DanConwayDev's Agent
nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0
PR description:
The checkpoint integration test can fail on a loaded builder because its 75 ms sleep leaves only 25 ms before the cold index expires. The nixpkgs 3.0.2 build observed `cold_expired == 1` where the test expected zero.
Simulate two minutes of downtime by editing the temporary checkpoint's `saved_at` field, with hot/cold retention periods of one minute and one week. Keep every existing assertion and leave production behavior unchanged. This extends the approach already applied to the equivalent unit tests in commit
|
||
|
|
49d94d8a05 |
Merge #54658b4c: ci: publish release tags to crates.io from Nostr CI
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs9gevtf3cnx2t8m5h7cxyzcda7ygcf9pld6kdkcq4l90sfcr2m45qvcet5s PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Crates.io publication is a manual release step even though Nostr CI already validates tagged commits. Publish after the existing lint, formatting and test steps succeed, using a repository-scoped CARGO_REGISTRY_TOKEN secret only in the publication step. Require a push event on a v-prefixed tag and an exact manifest-version match. Use Cargo package verification and --locked against the explicit crates-io registry. Only ngit-grasp is selected; grasp-audit remains unpublished. This assumes the coordinator has the scoped token configured and the tagged commit contains this workflow. It does not change archive/NIP-82 publication, stable promotion, documentation sync or registry versions; no live crate was uploaded during development. Validated workflow syntax with actionlint, matching/mismatched tag and missing-secret cases with an intercepted publish command, Cargo packaging without upload, cargo fmt --all -- --check, and git diff --cached --check. |
||
|
|
4b2d7511e1 |
Merge #a52fa551: ci(release): publish NIP-82 releases from tags
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs22ta92yrt4cmtklv9k5luw6t576d3z4nd0tfqud3s6swdcfrcxjgrmf5tu PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Motivation: make tagged ngit-grasp builds discoverable as complete NIP-82 releases without retaining the legacy zsp publication path. Approach: commit the application and asset manifest, install ngit through setup-ngit v3, and publish with the established dcdev nbunksec after reproducible archive packaging. Correctness: derive version and commit from the exact tag, enforce the expected application author and strict metadata, replicate to the pinned Primal, Ditto, and Haven Blossom servers, and publish to repository, catalog, and release relays. Security: place the CI nbunksec in a mode-restricted temporary file and remove it on exit; no signing material is stored in the repository or process arguments. Excluded: v3.0.1 predates this workflow and will be backfilled manually from its exact tag; automatic publication begins with the next genuine version tag. Validation: actionlint accepted the workflow, and ngit parsed the manifest before stopping at the expected dcdev publisher guard without uploading or signing. |
||
|
|
6864148989 |
Merge #6661b113: fix(sync): bounded historical repository mailbox sync
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsxvcd3z0zplr59xgkceza6vsh63etm6995e788ps2hvj5v3kt7s0c3lnsw4 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Repository roots and root-only descendants can exist on an accepted owner or maintainer's NIP-65 mailboxes without appearing on a declared repository relay. Root-participant provenance alone cannot discover that first root or query a known root against an unrelated maintainer mailbox. This proposal associates accepted public Full repositories with their owners and declared maintainers and probes their bounded, sanitized read/unmarked inboxes with repository-scoped historical mailbox work: exact repository coordinates plus every locally known root, on the existing paced, byte-bounded fetch path with independent per-relay progress. This coverage is deliberately historical-only: owning or maintaining a repository never adds an author's inbox relays to ordinary persistent live repository targets. Root authors retain the pre-existing live coverage on their own bounded inboxes, and participant-only mailboxes remain history-only with exact root provenance. Every peer-advertised NIP-65 relay URL first passes single-URL target hygiene (static outbound-target checks, temporary .onion rejection, nostr-watch's 52-word generated-path rule, 64-hex path segments, hostname-repeated-in-path, and a 2,048-byte wire-length ceiling), is canonicalized with the shared relay-key semantics, and is deduplicated before at most four relays per author and purpose are selected in published tag order. Canonical selection keys now match the connection map, fixing trailing-root-slash mailbox relays that previously could never start their history probe. Correctness depends on repository admission remaining authoritative and on every fetched event continuing through the normal write policy, persistence pipeline, and global NIP-09/NIP-62 tombstones. Private instances continue to withhold repository coordinates from public mailbox relays. Deliberately excluded are permanent owner/maintainer live subscriptions, shuffled or weighted relay rotation, NIP-66 ranking, host-level identity merging, whole-list kind 10002 rejection, and any new operator setting. Validated with nix develop -c cargo test (44 binaries, 2,876 tests, 0 failures), cargo fmt, and cargo clippy --all-targets with a clean report. |
||
|
|
681239c4b5 |
Merge #b6704561: feat(storage): retire migration backups family by fami…
feat(storage): retire migration backups family by family nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqstvuz9v8ewz6hfkpwssajjl9sjsxk50y39tlq30x75f9f8qy6wx8g94xt55 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Version 3 automatically migrates complete per-owner and `/prs/` repositories into thin views backed by one local object family per repository identifier. This PR retires each legacy family backup as soon as that family is proven healthy, so migration overhead is bounded by the family in flight instead of retaining a second full copy of the Git data indefinitely. ## Retirement safety - Every view must be correctly wired to its family and match the active migration journal snapshot. - The family must contain every Git-readable backup object, all packs must be indexed and pass `git verify-pack`, and the normal steady-state family integrity inspection must be healthy. This last check verifies readable/hash-correct objects, refs, alternates, packs, and shallow state before retirement. - A durable `retired` journal state is fsynced before deletion. Backup-directory removals and parent pruning are fsynced before the journal is removed, so restart recovery cannot lose track of a resurrected directory entry. - Unindexed packs are quarantined by SHA-256 content beneath `.grasp/migration/unindexed-packs/`. Identical retries converge; different payloads cannot overwrite one another even when their original filenames match. - Any active-migration gate error fails closed. A family that is structurally migrated but unhealthy keeps its backup, starts the service, logs an `ERROR`, and enters the ordinary asynchronous integrity-repair path. ## Shallow compatibility and repair A server-side depth-one fallback existed only in untagged development revisions from 2026-01-05 (`623cae5`) through its 2026-01-12 fix (`f25eea8`); no tagged v1 or v2 release shipped it. It ran only when pending state or PR objects had not arrived through the normal push path and the server fetched them from another listed Git server. V3 preserves any resulting `shallow` marker on the thin view, so shallow clones and the current tree remain as available as before migration, and keeps the legacy backup. The normal integrity worker automatically requests the full closure from accepted clone URLs. Successful repair removes the marker immediately and the next launch retires the backup; failed repair keeps both marker and backup and logs the affected identifier. No separate operator step is required for upgrade. ## Upgrade boundary The v2-to-v3 Git-data migration is one-way. V2 does not coordinate writes through shared families and must not run against migrated thin views. Rollback means restoring the pre-upgrade Git and relay-data snapshot together, not only replacing the binary. Installations already migrated by the earlier candidate are supported: manually removed verified backups leave completed journals that are compacted safely, while remaining backups are rechecked and retired family by family. Unverifiable stale backups are retained with a warning. GC and unreachable-object pruning remain deliberately excluded to preserve delete-state rollback material. Optional S3 storage is also separate from this local-storage PR. ## Validation Regressions cover corrupt loose family objects, repeated same-name unindexed packs with different content, durable interrupted retirement, active and stale journal states, manually removed backups, real depth-one migration, automatic closure recovery, marker retention on failed repair, and full client cloning after repair. `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test --workspace`, and `git diff --check` all pass on Rust 1.96. |
||
|
|
d73f2a3276 |
Merge #4584cec7: Route Git traffic through identifier families
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsytpxwcu3rtwvqntrk64dnekh6xf9czgv8sedtd8rd95fsugc53xcwp9h3l PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Completes the local identifier-family storage model after the prerequisite storage-primitives PR was merged. - Routes owner and `/prs/` reads, pushes, and proactive fetches through a shared `(object format, identifier)` object family. - Lets related repositories satisfy reachable SHA wants and advertises retained family base refs, avoiding repeat uploads of objects already stored by the server. - Migrates legacy repositories deterministically on launch while retaining rollback backups and preserving incomplete refs and readable objects. - Adds one permanent family integrity/healing engine for packs, object connectivity, view alternates, and ref targets. It fetches exact missing OIDs from clone URLs in accepted announcements through the existing hardened outbound path, rechecks the family, and logs unresolved damage at `ERROR`. - Runs that engine asynchronously after migration and exposes `ngit-grasp integrity-check --identifier <id> [--repair]` through a durable live-process request queue. Migration does not get a separate recovery subsystem: it performs the structural conversion, then hands the resulting family to the ordinary steady-state checker. Unindexed legacy packs remain in the rollback backup. Garbage collection, legacy backup archaeology, and S3 storage remain out of scope. Testing on gitnostr.com: the already-installed storage version makes structural migration a no-op, but the startup integrity pass still runs unconditionally, so this is a valid test of the permanent steady-state path. To prove remote self-healing, use a sacrificial identifier whose accepted announcement lists a second Git server containing the same reachable object; snapshot its family and views, move one verified loose object into quarantine, invoke `integrity-check --repair` or restart, and verify the repair log, restored object, `git fsck`, and a fresh clone. This does not re-test the first legacy-to-family transition; that transition should remain covered by the migration fixtures or a disposable pre-migration data copy. Do not remove the production migration marker to force a rerun. |
||
|
|
ba25b3a463 |
Merge #b6c5d12b: feat(http): support path-mounted GRASP services
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqstd3w39dut9hu325jnwfcvqa9r6y47t064hvenss0q30grw3pup7gz6ukp2 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Allow operators to share an authority by configuring a normalized public base path. Route HTTP, WebSocket, Git, and GRASP-06 traffic beneath that prefix and carry the path through service validation, generated relay identity, self-sync, NIP-11 icons, and landing links. Treat NIP-05 as a domain-root capability: path-mounted services neither expose the well-known document nor advertise NIP-05 in NIP-11, and generated owner profiles omit the nip05 field. Exact relay URL matching keeps sibling mounts distinct while clone URLs may descend below the configured prefix. This assumes reverse proxies preserve the configured public prefix. Proxy-side prefix stripping remains deliberately out of scope. Validation: - nix develop -c cargo test - nix develop -c cargo clippy --all-targets -- -D warnings - cargo fmt --all -- --check - git diff --check - nix-instantiate --parse nix/module.nix |
||
|
|
27f9eaaf98 |
Merge #c195deba: Add local identifier-family storage primitives
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsvr9w7ht8z9l8rw4glgpnu2xdh8ngtjwp8gvm4xpgwql6507vn9tqzylvuk PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Stacked on nostr:nevent1qqsw3zk0fdhqeummycnmmpzls0zwjzxmkfgnqgqnsje2hhdvvsgewkspz3mhxue69uhhyetvv9ujumn8d96zuer9wc07x838. Adds family paths and locks, thin repository views backed by Git alternates, anonymous negotiation bases, and retained refs. Local storage remains the default and deduplicates objects across every repository view sharing an identifier. |
||
|
|
927c38f059 |
Merge #e88acf4b: Design identifier-family Git object storage
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsw3zk0fdhqeummycnmmpzls0zwjzxmkfgnqgqnsje2hhdvvsgewksq2g90s PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Defines identifier-scoped object families shared by owner repositories and /prs/ views. The design accepts allowReachableSHA1InWant for related repositories, keeps local storage as the default deduplicated backend, makes S3 opt-in, and deliberately excludes garbage collection so deletion rollback remains recoverable. |
||
|
|
ce6e29ed21 |
Merge #31a50344: ci: cache Nix and Rust build inputs
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsrrfgrgj2lr8flyyquefg59jfgqze3h4h57e4f9keq32s8py6v3ts3ak2sv PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Reuse Nix store paths, Cargo sources, and Rust target artifacts across ngit-ci runs. Keys track the flake, toolchain, and Cargo dependency inputs while prefix restores preserve useful overlap after updates. Cache restore and save remain non-fatal optimizations, and the existing formatting, lint, and test commands are unchanged. Validated by listing the workflow with act and checking the resulting diff. |
||
|
|
e672255f69 |
Merge #3a2bb755: test(sync): isolate NEG proxy source traffic
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsr52ah25k3ypn380yqut6rypqmhyw5pux2hgv9w9kwrvgfur3kpngdqltr5 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: The NEG concurrency proxy records counters across all client connections. Once live self-subscription became reliable, the permissive archive source discovered the proxy URL in its own announcement and ran negentropy through the same measurement point, making the test evidence include scenery traffic. Reuse the production-outbound-policy archive source fixture already used by REQ. Its loopback event-directed targets are rejected while the syncing relay bootstrap remains operator-configured and allowed, preserving end-to-end coverage and attributing proxy counts only to the relay under test. Correctness assumes this integration topology remains loopback-only. Deliberately excluded: production sync behavior and the REQ scenario are unchanged. Validation: nix develop -c cargo fmt --check; cargo test --test sync sync::neg_concurrency::startup_historic_sync_stays_within_relay_neg_concurrency_limit; cargo test --test sync sync::req_concurrency::startup_historic_sync_stays_within_relay_req_concurrency_limit. |
||
|
|
b50883d9f0 |
Merge #2f5ad1ca: fix(logging): align operational severity with actionab…
fix(logging): align operational severity with actionability nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsz7kk3e29ceup4fzccjxq48r6vxu4f3jd54anfurevch0vp6lfmvq9fke8q PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Production evidence showed the default info stream was dominated by dependency chatter and peer-controlled detail: gitnostr emitted 1,008 per-repository discovery records in a 3,501-line window, the US relay emitted 232 in 972 lines, and the German relay emitted 2,288 discoveries plus 2,236 warnings in 11,472 lines. Rejected historical announcements dominated the warnings, while all three services were intentionally running NGIT_LOG_LEVEL=info rather than debug. This PR scopes bare levels to ngit-grasp with dependencies held at warn, while preserving explicit EnvFilter expressions. It moves individual repository, event, filter, WebSocket, HTTP disconnect, and missing-object probe diagnostics to debug; retains aggregate sync outcomes and capability fallback at info; retains transient cooldowns at warn; and keeps internal database, policy, service, and subprocess failures at error. Unsupported NIP-77 is reported once per connection rather than once per filter. Configuration docs, the NixOS module, the example environment, monitoring guidance, and changelog are updated together. Tests that use rejection diagnostics as an ordering barrier explicitly opt into application debug logging. A scheduler-sensitive checkpoint-expiry test encountered during the package build now simulates downtime through persisted state instead of a fixed sleep; production behavior is unchanged. Validation: - cargo fmt --check and git diff --check - focused logging, HTTP, Git, self-subscriber, capability-gate, downtime, and maintainer-reprocessing tests - all 795 library tests passed in the development suite - all nine maintainer-reprocessing tests passed sequentially after opting their fixtures into debug - the 2,500-event REQ concurrency stress test passed in isolation after an earlier host-contention timeout - nix build .#ngit-grasp --no-link passed, including the pinned-toolchain 795-test package gate |
||
|
|
2a2cabd78d |
Merge #21fd077e: fix(private-repos): restore self-subscription in priva…
fix(private-repos): restore self-subscription in private mode nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqszrlg80ct2jn7ednnam89yfsggeradp6wl27ncxsmwuqrd8746c5qll4q5e PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: The SelfSubscriber feeds the sync manager's repository index from the service's own accepted events. It dialled our own public WebSocket endpoint with an unauthenticated client, which a private instance's NIP-42 gate correctly refused: the gate runs in the HTTP layer, ahead of LocalRelay, and cannot distinguish our own process from any other anonymous dialler. Announcements accepted at runtime therefore never reached the sync index until a restart rebuilt it from the database, stalling proactive sync and the dynamic membership derived from accepted relay owners — worst on exactly the private-to-private mirroring GRASP-08 exists to enable. Attach the subscriber to the embedded relay in-process instead. A custom WebSocketTransport hands LocalRelay one end of an in-memory duplex pair and keeps the other, so the client gets an ordinary relay session with the same framing, subscription handling, and post-save broadcast, minus the listener, the auth gate, and the network round trip. This replaces the loopback dial in both modes: the public-mode feed is identical in content and strictly more reliable, and it removes a self-directed reconnect loop. Chosen over attaching the relay owner key as a NIP-42 authenticator plus adding the owner pubkey to the effective member set. That alternative works, but widens the member set and the authenticated surface to solve a problem that is not authentication: there is no remote party here. The in-process route needs no key, no membership entry, and no configuration, and nothing reaches the subscriber that the relay did not already accept and persist. Correctness assumptions: LocalRelay applies no NIP-42 or query policy of its own — private-mode access control lives entirely in the HTTP layer — so an in-process session is exactly a local session, not a bypassed remote one. Both ends speak raw WebSocket framing over the duplex with no HTTP upgrade, matching take_connection's Role::Server. The session consumes one connection permit, as the loopback dial did. The GRASP-08 regression test no longer restarts the relay over persistent LMDB: it publishes an announcement at runtime and asserts sync connections to both referenced relays, which is only possible if the live feed reached the index. Verified to fail against the previous implementation (60s deadline, no connections) and pass with this one. req_concurrency's source relay now applies the production outbound target policy. Its scenario lists a proxy URL in the announcement, and with a reliable live feed the source discovers that URL — a distinct host:port that happens to front itself — as an event-directed sync target and opens its own REQ traffic through the proxy, contending for a budget the test means to measure for the syncing relay alone. That behavior is pre-existing and was already reachable after a restart; only its timing changed. The policy keeps the source scenery without weakening the assertion. Deliberately excluded: neg_concurrency shares that topology but passes unchanged, so its fixture is left alone; the duplicate NIP-11 fetch between the pre-dial preflight probe and the post-connect hint fetch is untouched. Validation: cargo clippy --all-targets -D warnings; cargo test --lib (793 passed); cargo test --test private_mode --test sync --test outbound_policy --test purgatory_sync (255 passed, including the full sync suite under parallel load). req_concurrency's startup-burst test passed 4/4 isolated runs after the fixture change. |
||
|
|
8b69971593 |
Merge #0a6a0d75: feat(private-repos): authenticate outbound sync betwee…
feat(private-repos): authenticate outbound sync between GRASP-08 services nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsq56sdwh0cccm69uqln5ar58hf38xssqwpzs9fz87kfdtlv3x45qqr462cs PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Implements the outbound half of GRASP-08 private-service support: how an instance behaves as a sync client toward relays that demand authentication or advertise GRASP-08. Policy: (1) every sync connection answers NIP-42 AUTH challenges with the relay owner key when available, on public and private instances alike; after successful authentication the refused subscription is retried once, and without an owner key authentication is skipped and auth-demanding subscriptions park immediately. (2) A restricted: rejection after valid authentication is terminal and parks subscription work via the existing policy-refusal machinery, so there are no retry storms. (3) A relay whose NIP-11 supported_grasps advertises GRASP-08 is recognised with a pre-dial NIP-11 fetch that re-runs the outbound target policy, keeping the SSRF gate intact: a public instance parks such a relay without ever opening the WebSocket (no AUTH exchange, nothing to launder), while a private instance treats it as a peer and attaches the GRASP-08 repository-root NIP-98 credential (60-second validity, minted fresh per git subprocess) to purgatory Git fetches from that host, both signed with the relay owner key. (4) Relays with missing or unreadable NIP-11 remain ordinary sync targets. (5) Membership tightening: the NIP-11 owner of a relay referenced by an accepted announcement is only admitted as a derived member when that relay also advertises GRASP-08; configured NGIT_PRIVATE_MEMBERS are unaffected. NIP-42 here is identification, not confidentiality: our pubkey is already published via NIP-11 and NIP-05, and a private instance authenticating outbound discloses its identity consistently with the public-discovery stance of GRASP-08. Also adds disclosure guidance for private services: security contact via NIP-11 contact and the NIP-05 root identity, reports as NIP-17 encrypted DMs on the public mailbox relays of maintainers, collaboration access by adding the reporter to NGIT_PRIVATE_MEMBERS, and deliberately no non-member submission lane. Validation: cargo fmt --check, cargo clippy --all-targets -D warnings, cargo test --lib (792 passed), and the private_mode, sync, outbound_policy, purgatory_sync, nip11_document and relay_identity integration suites all pass. |
||
|
|
d248394bcf |
Merge #2568f4fc: ci: skip docs-only pushes with paths-ignore
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsz2685lna6yk59uk6u0elwaejhjw3gj32fdrvnh8kep2tu78l34eqr838a3 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Every push currently runs the full Rust CI pipeline, including pushes that only touch documentation, markdown, or gitignored-except-README session notes under work/. Add paths-ignore for docs/**, **/*.md, and work/** to the push trigger so those runs are skipped, while a push touching both code and docs still runs (paths-ignore only skips when every changed file matches). Per ngit-ci's filter semantics (ngit-ci docs/writing-workflows.md): paths filters are supported for push triggers with GitHub glob syntax; tag pushes always run regardless of path filters; and pull_request path filters are not evaluated yet, so the pull_request trigger is left unfiltered deliberately. Validated by parsing the workflow YAML; no job content changes. |
||
|
|
5865ed4a47 |
Merge #f7cecf4c: test(sync): replace remaining fixed sleeps with observ…
test(sync): replace remaining fixed sleeps with observable waits nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs00nk0f3vjr5l784yf6lcc7zultsy8asa82znqk6dq0mpk2m5kpfsl6hg0r PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Sweep the fixed sleeps left in the sync suite after the tag_variations / live_sync hardening, following the same pattern: wait on the condition the test actually cares about with a bounded deadline, so healthy runs are no slower and only the failure bound widens to 30 seconds. - discovery, historic_sync, metrics, maintainer_reprocessing: replace "wait for discovery/sync" sleeps with bounded polls on the synced event, a metrics counter/gauge, or the syncing relay's log (rejected announcements are observable by their note ID), and widen the short verification deadlines that followed them. - purgatory_fetch, live_sync, historic_sync: replace fixed post-connect sleeps on raw clients with `connect().and_wait(timeout)`. - metrics: poll the metrics endpoint for readiness instead of sleeping through relay startup. TestClient::send_event is reworked to send through its single tracked relay so the SDK error kind survives: transient transport failures (disconnect, timeout, not-connected) are retried with the existing bounded backoff plus a reconnect, while a relay OK-false rejection now fails immediately instead of being retried, so tests cannot mask write-policy regressions. A transient client error under load previously killed adaptive_pagination's seeding despite the retry loop, because rejections and transport failures were indistinguishable at the pool level. Deliberate time-as-behavior sleeps stay and are now commented: the one-second rejected-hot-cache TTL expiries, created_at spacing before +1s-dated events, and fixed observation windows for events that must NOT appear. req_concurrency is deliberately untouched: its proper fix needs a relay observable for descendant-sweep completion (issue b052fa70). Validated under CI's hostile global git config (GIT_CONFIG_GLOBAL mirror of the workflow step): three consecutive green runs of `cargo test --locked --test sync`, plus `cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D warnings`, and a full `cargo test --locked` (its sync pass green a fourth time; one unrelated load flake in live_sync_regroups_after_filter_count_refusal passed on isolated rerun and in the repeat full run). |
||
|
|
22abda22a8 |
Merge #74151bb5: style: apply rustfmt to private-repos code
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs8g9gmk5erhxf2m28lh96lrcnqh2p995jwzgn9z6wn2xk7dgz68jg56dksy PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: The GRASP-08 service-auth PR branched before the pull_request CI trigger existed, so no PR run checked it, and the membership-gating PR was merged with a failing run. Both landed unformatted code, so every master push now fails at cargo fmt --all -- --check before clippy or the test suites execute. Mechanical cargo fmt --all output only; no behavior change. Any clippy or test failures the fmt gate was hiding are addressed separately. |
||
|
|
4be9408aed |
Merge #bd5dda7a: feat(identity): publish relay owner events
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqst6hw60gzs9qcxzr95h9wd76n3tctq9khdgpw2gxd75glsstmfvjgkn9xwt PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: This PR makes the relay-owner key discoverable and usable as the ngit-ci service identity. On startup the relay signs a minimal kind-0 NIP-05 bot profile (scheme-less public URL as name, `_@domain`, `bot: true`) and a kind-10002 list naming this relay as its sole read/write relay, then publishes them to the configured user-index relays in the background with a capped retry backoff. Publication is deliberately conservative. No identity event — locally stored or freshly generated — is sent before the local database and at least one user-index relay have been successfully checked for that kind, and every send is preceded by a per-relay re-check. An identity found on an index relay is adopted locally (replaceable semantics keep the newest copy) and never overwritten, so the relay only fills gaps on index relays that confirm they hold none: a database wipe followed by a boot during an index outage can no longer displace a customized profile surviving on the indexes, and pushing profile updates onto an index that already has one is left to the operator's own client. A kind with no local copy is not even seeded until a reachable index confirms it holds no identity of that kind. Remote outages and local rejections never block startup. In private mode (GRASP-08) the identity events are seeded and served locally but never published, so a private relay does not advertise its existence. The write policy trusts owner-signed events only for kinds without a dedicated admission policy, such as ngit-ci coordinator advertisements that carry no repository root tag. Owner-signed NIP-34 announcements, state events, and PRs run the normal validation, ref alignment, and purgatory handling like any other author; the relay's own kind 0/10002 identity is always accepted; and the NIP-09/NIP-62 deletion gate runs before any owner acceptance, so a replayed retracted owner event cannot undo its tombstone. Because the event blacklist cannot block the owner key, rotating it is the only remediation if it is compromised. Integration coverage includes local seeding and indexing, restart behavior with an operator-customized profile, adoption from a user index after a database wipe, deferral while no index is reachable, non-displacement on a recovering index that already holds an identity, private-mode suppression, and owner-signed state events taking the normal admission path. Validated with rustfmt, strict workspace Clippy, and the full workspace test suite. |
||
|
|
e173c58e67 |
Merge #9af177ea: feat(private-repos): gate announcement admission on me…
feat(private-repos): gate announcement admission on membership nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsf4uthaf2cr5hes246tckccufhntfj3uc5su95t5l7z5p9g6s3jmq059w44 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: In GRASP-08 private mode, accepted kind-30617 announcements expand hosting and - via their referenced relays NIP-11 owners - the effective member set itself. Announcement admission never checked the author, so a member could submit a third-party-signed announcement (or sync could import one from an operator-configured source) and thereby mint membership for pubkeys no member ever chose. This PR closes that amplification loop: in private mode a repository announcement is only admitted when its author (event pubkey) is a current effective member (configured members plus admitted relay owners, via the shared PrivateAccess set) evaluated at admission time. The gate sits at the single choke point all arrival paths funnel through - direct publish, sync import, purgatory entry - and rejections use the existing announcement rejection machinery. Public mode is unchanged, no new configuration is added, state events (30618) keep GRASP-01 maintainer rules, and removal is non-retroactive: repositories admitted while their author was a member stay hosted until the operator curates them. Also documents the rationale in the GRASP-08 design doc and architecture doc. Deliberately excluded: outbound authentication when syncing from other private services, which remains a separate follow-up. Validation: cargo clippy --all-targets -D warnings clean; lib tests (783), private_mode (52, incl. a new member/non-member admission integration test), nip34_announcements (60), repository_creation (47), and purgatory (55) suites all pass. |
||
|
|
2a213baee9 |
Merge #abd26762: test: survive hostile git config and non-reaping PID 1…
test: survive hostile git config and non-reaping PID 1 in CI nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs2h5n8vgaysdav2nwh35f3rs3vvm6kjc0slv4d87jz56cepzusavc4xc29f PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Fixes the 7 lib-test failures seen in CI on master and on the pull_request-trigger PR. Five tests built git fixtures with bare git commands, so the hostile pre-commit hook CI installs via core.hooksPath broke fixture commits silently. Fixtures now use the hermetic grasp_audit::git_command() and assert success on every step. Two process-group tests probed descendant death with kill(pid, 0), which still succeeds for an unreaped zombie; act containers have no reaping PID 1, so the orphaned descendant stayed a zombie and the probes timed out. The probe now also treats an unreaped zombie (state Z in /proc/<pid>/stat) as terminated. Both failure modes were reproduced and verified fixed locally under the workflow hostile git config plus a non-reaping subreaper wrapper. fmt, clippy -D warnings, the full workspace suite and grasp-audit all pass. Stacked on nostr:nevent1qqsg7maf57s5v7u5cnvrvg8n3dl8dluremlmtqxwn9ex3rvmtrrjaqcpz3mhxue69uhhyetvv9ujumn8d96zuer9wc45dzcp so the pull_request trigger is present and CI runs against this PR. |
||
|
|
602f38878c |
Merge #3fa7d8ad: feat(private-repos): add GRASP-08 service authenticati…
feat(private-repos): add GRASP-08 service authentication nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsrlf7c45djx8qdst0jg764n8n6wfrf05gffncjfnym3ceguy6k3pgezg30q PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Implements the mergeable single-service GRASP-08 authentication boundary on the existing canonical repository paths. GRASP-08 does not require another path component: Git remains at /<npub>/<identifier>.git, while service-fleet routing is deliberately left for a future proposal. Private mode fails closed. WebSocket access authenticates with NIP-42 before traffic reaches the relay, and Smart HTTP uses the reusable repository-scoped GRASP-08 form of NIP-98. Missing, malformed, expired, or non-member credentials receive the same empty 401 response. The canonical public origin is operator-controlled, paths remain contained under the Git root, and ordinary GRASP-01 push authorization still applies after service authentication. Membership combines explicitly configured pubkeys with the NIP-11 owners of relays referenced by accepted repository announcements. It reuses the existing once-per-connection NIP-11 fetch and five-second maintenance pass, so the feature adds no polling, subscriptions, relay connections, or background task. Owners learned only through unaccepted purgatory state cannot grant access. The three atomic commits separate the authentication boundary, dynamic accepted-relay membership, and subprocess integration coverage. Configuration is synchronized across source, reference docs, NixOS module, and example environment; architecture documentation records the implemented single-service scope. Validation: - cargo test --locked --lib: 759 passed; - private service subprocess scenario: 45 passed, including missing/non-member rejection and member admission; - Smart HTTP streaming: 3 passed; - NIP-01/WebSocket compliance: 52 passed; - cargo clippy --all-targets -- -D warnings: passed; - nix build .#ngit-grasp: passed. Resource assessment: inactive private mode has no runtime path change. Active private mode adds one shared membership set, one generation watch, per-session NIP-42 bridging, and membership reconciliation on the existing maintenance cadence. Its relay-owner discovery reuses NIP-11 state already fetched for sync limits. Recommendation: ready to merge as the GRASP-08 single-service foundation. Multi-service fleet orchestration and encrypted kind-10318 client discovery remain explicit follow-up scope. |
||
|
|
ae22bcc1f2 |
Merge #8f6fa9a7: ci: run Rust CI on pull_request events
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsg7maf57s5v7u5cnvrvg8n3dl8dluremlmtqxwn9ex3rvmtrrjaqcm30vm0 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: The workflow only fired on push, so Nostr PRs (kinds 1618/1619) received no CI runs until their commits landed on a branch. ngit-ci's pull_request trigger fires on PR and PR-update events, giving contributors feedback before merge. The job itself is unchanged: the same checks now also run against the PR head commit. No branch filters are added, matching the existing unfiltered push trigger. |
||
|
|
b4f554ad98 |
Merge #edf4290b: fix: disable ANSI log colors when stdout is not a term…
fix: disable ANSI log colors when stdout is not a terminal nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqswmapfpw7etaxy5zhl5ltwlkhrycuhehjzdqztmupze0nxnype4vcuqwe07 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Motivation: four sync integration tests failed deterministically in some environments: proactive_sync_plus:: missing_relay_list_uses_bounded_fallback_coverage, maintainer_reprocessing:: unresolved_repositories_share_one_dependency_poll_per_relay, metrics::test_live_sync_event_count, and reconnect_backoff:: flapping_relay_handshakes_do_not_reset_exponential_backoff. Each waits for a plain `field=value` substring (e.g. `consecutive_failures=1`) in the relay subprocess log written by the TestRelay fixture. The tracing fmt layer colorizes by default, so the redirected log contained `consecutive_failures^[[0m^[[2m=^[[0m1`, which never matches. tracing-subscriber honours NO_COLOR, so the same tests passed or failed depending on the invoking environment: runs with NO_COLOR set produced clean logs, interactive-launched runs produced ANSI logs and deterministic failures. Approach: initialize the fmt layer with `.with_ansi(std::io::stdout().is_terminal())`. Colors are emitted only when a human is watching a terminal; redirected output (test fixtures, journald, pipelines) stays plain. This is a logging hygiene fix in the binary, not a test accommodation: the defect was writing terminal control sequences to a non-terminal stream. Correctness assumptions: the relay binary is the only process whose logs the fixtures scrape, and tests always redirect its stdout to a file, so is_terminal() is deterministically false there and the log format no longer varies with the parent environment. Excluded scope: several sync tests remain flaky under full-suite parallel load but pass repeatedly in isolation (req_concurrency::startup_historic_sync_stays_within_relay_req_concurrency_limit, live_sync::live_sync_regroups_after_filter_count_refusal, tag_variations::test_layer3_sync_with_lowercase_e_tag). They assert on metrics or proxy behaviour, not log text; their bounded deadlines are exceeded when the whole suite competes for CPU. Left undiagnosed rather than papered over with wider timeouts. Validation: the four log-scraping tests pass individually and in full runs after the fix; cargo fmt makes no changes; cargo clippy --workspace --all-targets -- -D warnings is clean; full cargo test is green except the pre-existing load flakes above, each of which passes 3/3 when run in isolation. |
||
|
|
18eddeedb5 |
Merge #4e732958: feat(sync): discover participant mailboxes
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsyuueftzc68d5a3x5d0pecs66y0nacv4pc87kkcfw499adyl5t3ycxqgtta PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: ## What this adds Repository conversations can continue on relays used only by a non-root participant. For example, a reaction to an issue may be stored on one relay, while a reply to that reaction exists only on the reaction author's NIP-65 write relay. Root-author relay discovery and recursive reference queries cannot find that reply unless the participant's mailbox is also searched. This PR adds bounded, historic participant-mailbox coverage: 1. Preserve exact repository-root provenance while deriving the accepted recursive descendant frontier. 2. Treat authors of accepted replies, comments, reactions, zaps and other descendants as thread participants. 3. Accept kind `10002` relay lists only for repository owners, maintainers, root authors and those accepted participants. 4. Derive each participant's NIP-65 read, write and unmarked mailbox relays. 5. Query those mailboxes only for the repository roots and accepted descendant IDs associated with that participant. 6. Pass every returned event through the ordinary write policy, deletion/replacement rules and persistence pipeline. This discovers replies to reactions and other indirect descendants without fetching a participant's unrelated notes or trusting their relay list to bypass repository policy. ## Bounds and scheduling - The existing `NGIT_SYNC_RECURSIVE_DESCENDANT_LIMIT` bounds which indirect descendant IDs remain query roots. Each direct event that tags a repository root has its own bounded branch; the default is 500. - Once a branch reaches that bound, later indirect descendants cannot add more participant authors or query roots through that branch. - Exact root provenance prevents roots from one repository leaking into another participant query. - Participant mailbox coverage is historic only. It does not add permanent non-root participant subscriptions or expand the existing live-sync tier. - Filters use the existing byte-bounded grouping and paginated `RelayConnection::fetch_events` path, including request pacing, background priority, subscription-ledger capacity and EOSE/CLOSED handling. - At most one mailbox worker runs per relay, and at most one new due relay is started per maintenance pass. - Relays progress independently: there is no global mailbox lane, cross-relay success condition or shared completion counter. ## Restart and failure behavior - A probe starts only after both the WebSocket and the sync actor's connection lifecycle are ready. - Ready due relays are preferred, so old unreachable mailbox sources cannot starve connected work. - Completed mailbox workers are handled before new connection results, so a large startup connection queue cannot delay cursor progress or resource release. - Successful filters advance an in-memory, stable-sorted cursor immediately. Completing the last filter schedules the next historic rotation after 24 hours. - Failed filters release their relay worker and retry after five minutes without blocking other relays. - On restart, accepted roots, participants and kind `10002` ownership are reconstructed from LMDB. Filter cursors deliberately restart from the first current group; they are best-effort coverage, not durable exactly-once state. The implementation reuses the existing relay transport. It does not add a mailbox-specific protocol state machine, pending-batch purpose, CLOSE API, NIP-42 retry system, watchdog or connection-queue coordinator. ## Coverage and production evidence The audit motivating this change found 72 events visible through gitworkshop but absent from the pre-change production relay across the `gitworkshop` and `ngit` repositories. The persistent canary served 29 exact misses: six kind-1 notes, three reposts, ten reactions, six comments, two issues, one zap receipt and one repository-follow event. The exact candidate `de5fa6625aa5693afcde7335a572456680aef9df` activated on the isolated archive at 2026-08-14 19:23:49 UTC. Restart reconstruction found 4,291 accepted roots, 403 participant authors and 351 mailbox relays. During the recorded soak: - 63 mailbox filters started and all 63 reached terminal handling: 59 successes and four bounded failures. - Failures received the intended five-minute retry and did not block successful progress on other relays. - `relay.ngit.dev`, `nostr.land`, `nostr.mom`, `haven.danconwaydev.com/inbox` and other relays advanced independently. - `nostr.azzamo.net` completed all six filter groups; the final terminal reported `completed_cycle=true` and scheduled the next probe in 86,400 seconds. - The archive remained active with zero restarts and no process panic or fatal error. The temporary archive test override of `NGIT_SYNC_RECURSIVE_DESCENDANT_LIMIT=2` has been removed. Both the archive and public gitnostr.com have no override and therefore use the default of 500. Public gitnostr.com was not restarted or changed by the archive deployment. ## Validation and review state - Exact tested head: `de5fa6625aa5693afcde7335a572456680aef9df`. - One commit; 1,157 additions and 140 deletions across 11 files. - 767 library tests passed in the exact Nix release build locally and on the production host. - All three proactive Sync+ integration scenarios pass together, including a child found only through a participant reaction author's write mailbox. - Strict all-target Clippy, formatting, diff checks, Nix flake evaluation and a conflict-free merge-tree check against current `master` passed. - One remote build attempt hit the pre-existing `test_entries_expired_during_downtime` timing flake; its source is unchanged by this PR, and the unchanged candidate passed all 767 tests on retry before activation. Recommended for merge. The archive demonstrates successful and failed terminal paths, per-relay independence, restart reconstruction, cursor advancement and a complete historic-to-24-hour-refresh rotation under real startup load. |
||
|
|
97bf3e89c2 |
Merge #cea1ad82: feat(http): serve NIP-05 root identity
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsvagddsgtyqp03tjctp3p405ajqq7zxphcrej3mnu4j4kc5rkyflqnxtu05 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Relay operators already have a stable Nostr identity for NIP-11 and NIP-42, but clients cannot resolve the domain itself to that key. This proposal serves the NIP-05 root identifier so `_@domain` identifies a root-mounted relay without another key source. It adds an exact root-level `/.well-known/nostr.json` route for GET and HEAD, serializes the relay-owner public key as lowercase hex under `names._`, retains permissive CORS, and gives this route precedence over NIP-11 content negotiation. NIP-11 derives support from the relay request path: only `/` advertises NIP-05, while nested relay paths omit it. The changelog, architecture, README, and relay-owner configuration descriptions are synchronized. The path check assumes the HTTP request path is the relay public mount path; a future explicit base-path setting should remain the source for this decision if proxy routing hides that path. Named identities and recommended relay-hint mappings remain outside this change. Validated with 760 library tests, the root/non-root NIP-05 server integration test, strict all-target Clippy, cargo fmt, git diff checks, and Nix module parsing. |
||
|
|
014875cb9f |
Merge #26412457: feat(sync): recurse through related event frontiers
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqszvsfy2lca5ujjnpn3h0kft403m5nnxqj6nsd68m394vlj9c8xjlc4hc37z PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: ## Scope Implements the recursive related-event frontier recommended by nostr:nevent1qqsfdj4fegeq7zjpje7cht6cj9xcsw5a9h2lcx2ju0f4uj80gsvvyhcpz3mhxue69uhhyetvv9ujumn8d96zuer9wc70zgv8. PR1 and PR2 are merged. This is layer three; participant mailbox discovery remains the final stacked proposal in nostr:nevent1qqsyuueftzc68d5a3x5d0pecs66y0nacv4pc87kkcfw499adyl5t3ycpz3mhxue69uhhyetvv9ujumn8d96zuer9wca2tmqx. Root-only and one-generation filters miss events which reference an immediate parent, including replies to reactions. This layer traverses accepted local `e`, `E`, and event-valued `q` references plus `a`, `A`, and address-valued `q` references. The resulting frontier feeds existing live coverage when it fits and the existing five-second REQ+EOSE historic rotation when it does not. ## Simplified bounded model Let `R` be an accepted repository root and `D` an event which directly tags `R`. - `D` owns one independent recursive subtree allowance, configured by `NGIT_SYNC_RECURSIVE_DESCENDANT_LIMIT` / `--sync-recursive-descendant-limit` and defaulting to 500. - `D` itself is direct/core coverage and does not consume the allowance. Every other event which directly tags `R` starts its own branch. - Locally stored descendants are walked breadth-first in stable creation-time/event-ID order through event and address references. - While fewer than 500 recursive descendants are known for a branch, its members can be child-query seeds. - As soon as the branch reaches 500, none of its members remain in recursive child-query filters. Existing auxiliary live coverage is closed and rebuilt without that branch; historic filters omit it too. - A recursively discovered event never creates another independent allowance. The existing eight-generation traversal remains an additional safety bound. - Direct/core sync and unrelated direct-root branches continue normally after one branch fills. This is intentionally a soft query-frontier limit, not an exact storage cap. Events returned by a request already in flight are still processed normally. A single response could therefore store more than the configured number of siblings; on the next reconciliation the branch is full and none of those events can extend recursive fan-out. The design bounds continued graph expansion without adding receive-path rejection or reservation machinery. ## Relay independence and restart There is no new cross-relay coordinator, counter, reservation, teardown dependency, or completion dependency. Each source retains its own connection, subscriptions, historic cursor, EOSE handling, pacing, health, and backoff. The only shared input is the relay's existing local database. Every connection derives its desired frontier from accepted LMDB events during normal reconciliation. An event arriving from one source can therefore make the locally reconstructed branch full for later reconciliation on every source, but no source waits for or coordinates with another. LMDB is also the restart checkpoint. Startup derives the same bounded frontier from persisted events; a branch already full issues no further descendant queries and receives no fresh allowance. No additional durable cursor or budget state is stored. ## Configuration The positive integer option is present on all four supported surfaces: - CLI: `--sync-recursive-descendant-limit` - environment: `NGIT_SYNC_RECURSIVE_DESCENDANT_LIMIT` - NixOS: `syncRecursiveDescendantLimit` - example environment and configuration reference Default: 500. Small values such as 2 or 3 are supported for integration tests and fresh-archive canaries. ## Validation - Exact head: `9360d419b3bffc5703a90aae330c315ed22459ee` - Stable PR root: `26412457f1da725298671bbec95d5f1dd2733025a9c1ba3ee25ab3f22e0e697f` - Final diff: 667 additions, 110 deletions (777 changed lines), down from the discarded 1.3k-addition exact-admission design. - `nix develop -c cargo test --lib -q`: 758 passed. - Six focused recursive-frontier unit tests: passed. - Limit-2 historic/restart integration: passed in 42 seconds. - Serialized sync suite: all descendant cases passed; overall 95 passed, 1 deliberately ignored, and 2 unrelated Layer-2 timing tests failed once then passed isolated reruns. - `cargo clippy --workspace --all-targets --all-features -- -D warnings`: passed. - `cargo fmt --check`, `git diff --check`, and `nix flake check --no-build`: passed. The real-relay integration constructs `R <- D <- A <- B <- C` with a limit of 2. It observes `A` and `B`, proves `C` is not queried, restarts against the same LMDB, publishes another child of `B`, proves both excluded children remain absent, and proves an unmetered direct/core marker still syncs. ## Why the model changed An aggregate 4,096 limit truncated unrelated direct sets. A later 32,768 recursive aggregate still allowed one popular direct child to consume the entire repository budget. The discarded exact per-branch design prevented overshoot but added roughly 680 runtime lines of receive-time admission, reservations, and cross-relay lifecycle handling. This head keeps the useful per-direct-child boundary and deliberately sacrifices an exact stored-event guarantee for a much smaller, reconstructible query-frontier rule. The final PR is a single atomic commit and adds no inter-relay behavior. ## Coverage evidence The audit found 17 genuine missing events at recursive depths 3–5 in the named `gitworkshop` and `ngit` repositories, demonstrating that the former one-generation rule was incomplete. A superseded unrestricted recursive canary recovered at least 230 saved events, showing material recursive yield; it is not presented as validation of the final tiny-limit boundary. ## Canary deployment The combined PR3 + stacked PR4 candidate was deployed only to the isolated archive canary at 2026-08-14 16:09:48 UTC. PR3 is exact commit `9360d419b3bffc5703a90aae330c315ed22459ee`; the deployed binary is `/nix/store/wlhqpr0x18njpmj3khwigrzhwj9z48lq-ngit-grasp-2.1.2/bin/ngit-grasp`. The canary uses persisted LMDB at `/persistent/grasp/gitnostr-com-archive-soft-cap-2`, `NGIT_SYNC_RECURSIVE_DESCENDANT_LIMIT=2`, and the normal real-relay archive inventory. At the 2026-08-14 16:13:08 UTC snapshot it was active with PID 1553732 and zero restarts. Since activation, logs contained five started descendant historic queries and six terminal descendant batch transitions, demonstrating startup reconstruction and live REQ+EOSE descendant operation. No application panic, OOM, failed assertion, or process crash was present; the only textual `fatal` match was an unrelated external Git remote TLS hostname error. The public `gitnostr.com` service remained on its prior binary and PID with zero restarts. This is deliberately described as an early three-minute operational snapshot, not a completed long soak or proof that every naturally occurring branch saturated the limit. ## Review state Merge-ready. The simplified implementation, deterministic limit-2 restart integration, full validation above, and early isolated real-relay canary support a merge recommendation. Extended canary saturation/restart evidence and production deployment at the safe default of 500 remain operational follow-up checks and are not claimed as completed here. |
||
|
|
ffe93bd089 |
Merge #a26eee65: fix(sync): retry related policy orphans
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs2ymhwv5e62ckys4n94ggqu07tl8f83t0hlk3wun2d9yvzx9w299szfklkk PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Stacked on nostr:nevent1qqsqzt6cxx9aeaj7wq5tr3mdne5e32ztpcmgvhpksz2jdyk0m20fksspz3mhxue69uhhyetvv9ujumn8d96zuer9wctgxcg4 and implements the durable dependency-retry layer from nostr:nevent1qqsfdj4fegeq7zjpje7cht6cj9xcsw5a9h2lcx2ju0f4uj80gsvvyhcpz3mhxue69uhhyetvv9ujumn8d96zuer9wc70zgv8.\n\nRepository-related events can legitimately arrive before the accepted event or repository address that makes them admissible. This change retains those policy orphans in the existing crash-safe rejected-event checkpoint and retries them in both directions whenever a dependency is saved.\n\nThe retained queue is deliberately bounded: 1,024 events, 8 MiB of serialized event payloads, 128 KiB per event, seven-day expiry, and 512 attempts per triggered closure. Oldest-first eviction is deterministic. Only terminal processing outcomes remove an entry; still-dependent and persistence outcomes remain pending.\n\nLocal evidence:\n- cargo fmt --check: pass\n- cargo test --lib: 754 passed\n- cargo clippy --all-targets -- -D warnings: pass\n- restore, forward/backward dependency matching, count-bound eviction, and end-to-end child-before-parent recovery are covered\n\nArchive canary evidence for exact commit 08d38fde:\n- deployed as 2.1.2-08d38fde at 2026-08-13 23:42:37 UTC\n- active/running with zero restarts; production remained on the parent PR\n- 26,636 deliveries observed across relay.ngit.dev and gitnostr.com after five minutes: 26,587 duplicates, 32 purgatory, 16 saved, one tombstone, zero rejected outcomes, and zero persistence errors\n- the retained-state sampler exported related_dependency_events=0 and sampled pipeline queue depth remained zero\n- no candidate-local panic, fatal, or persistence failure; channel-closed lines at 23:42:36 belong to shutdown of the previous process\n\nProduction evidence:\n- promoted unchanged at 2026-08-13 23:51:56 UTC as 2.1.2-08d38fde\n- active/running with zero restarts; archive start time remained unchanged\n- first 6,450 live deliveries were all duplicates, with zero rejected/persistence outcomes and queue depth zero\n- no candidate-local panic, fatal, or persistence failure in the post-start journal\n\nReady for merge as the second layer of the stack. Recursive relay fetching and participant mailbox discovery remain deliberately separate follow-up layers. |
||
|
|
d1b46a7cee |
Merge #012f5831: fix(sync): account for hydration outcomes
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsqzt6cxx9aeaj7wq5tr3mdne5e32ztpcmgvhpksz2jdyk0m20fkss777nwc PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: ## Scope Adds explicit Proactive Sync Plus hydration outcome accounting and keeps dependency-sensitive or transient exact-ID failures pending instead of silently counting delivery as persistence. Tracks nostr:nevent1qqsfdj4fegeq7zjpje7cht6cj9xcsw5a9h2lcx2ju0f4uj80gsvvyhcpz3mhxue69uhhyetvv9ujumn8d96zuer9wc70zgv8 ## Local validation - cargo test --lib: 750 passed - censored-relay incomplete exact-ID recovery scenario: passed - sync metrics integration suite: 11 passed - cargo clippy --all-targets -- -D warnings: passed - exact Nix candidate package: full release-profile test suite passed on rerun; one untouched 100/200 ms TTL test was timing-flaky on the first run ## Archive canary Deployed 2026-08-13 23:06:19 UTC as 2.1.2-39e1f692 from /nix/store/api7j4h6x7724cjm3qz78x100m52r1zb-ngit-grasp-2.1.2. The archive stayed active with zero restarts. A clean pipeline window recorded 2,350 deliveries, 2,322 duplicates, 24 purgatory, 4 saved, zero rejected, zero persistence errors, and queue depth zero. The longer burn-in demonstrated the diagnostic value directly: relay.ngit.dev requested 900 recovery IDs and delivered none, while relay.poster.place requested 90, delivered 90, and saved all 90. The only error-text was expected third-party DNS/TLS/WebSocket noise. ## Production evidence Promoted 2026-08-13 23:22:29 UTC as public and loopback NIP-11 version 2.1.2-39e1f692 from /nix/store/iq80zz3zi5h8l6q21dcfnj9b6sqvkmxa-ngit-grasp-2.1.2. The unit is active with zero restarts. Its first pipeline window recorded 1,039 deliveries, 1,037 duplicates, one purgatory event, one explicit invalid rejection, zero persistence errors, and queue depth zero. No candidate-local panic, fatal, or persistence failure appeared after startup. ## Review state Ready for review and merge. This is the base of the repository-coverage stack; later proposals will depend on it and cite this proposal. |
||
|
|
79f1d3b993 |
Merge #be6422ae: security: bound peer-controlled transient state
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqstuepz4mknkrlxznjptkwkeu4c2up5ya23tun8ke7rlr7qdptzdsc2ae0fd PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Issue nostr:nevent1qqsv09nf4dy084sk8vyl5yyg5v6e3ujfrxmuc93zmrc68nqeayup62cpz3mhxue69uhhyetvv9ujumn8d96zuer9wca4ndyx follows the auth-required OOM incident by auditing every transient collection whose lifetime can be influenced by a peer. This stack fixes four concrete retention surfaces: - EOSE/CLOSED actor notifications now have fixed backpressure while the independent terminal lane still releases permits; - auth retry markers accept only subscription IDs owned by the current session ledger; - deferred consolidation processes its deduplicated actor-owned state directly instead of accumulating self-wakeups; - connection and NIP-65 worker result channels encode their existing producer bounds. It also exports fixed-cardinality aggregate gauges for important long-lived sync state and adds the architecture inventory recording producer, cleanup owner, cardinality class, terminal behavior, and operational signal for each subsystem. Normal retained event/database growth remains out of scope. Validation: - `cargo test --locked --lib`: 749 passed; - serialized `cargo test --locked --test sync -- --test-threads=1`: 96 passed, one deliberately ignored; - formatting and warnings-as-errors clippy passed throughout the atomic commits; - gitnostr.com is running exact revised tip `70dec5ac295dbedd340a9be5a8db873f4b4040fb`; - production startup exercised 173 queued canonical relay attempts while execution remained under the existing eight-worker semaphore; the corrected gauge distinguishes this externally bounded queue from simultaneous dials; - retained state remained small under startup traffic: four pending batches/subscriptions, zero auth retries and deferred consolidations, ten temporary dependency relays, 86 purgatory dependency attempts, and eight descendant rotations; - zero service restarts; memory settled around 1.21 GiB with a 1.22 GiB peak; - no lifecycle-channel saturation, forged-auth growth, panic, or proposal-related application error was observed. SDK error logs were malformed third-party events, unsupported NEG responses, an ordinary local reset, and an expected private-relay auth rejection. Recommendation: ready to merge. |
||
|
|
3f621f3cbc |
Merge #3d857827: fix(http): account trusted proxy client addresses
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsrmptcyasq39jujn9qhhjekftqyy3j9hkedjzkq29sawm5tkqmw4qtgy9zx PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Reverse-proxied production deployments currently attribute every WebSocket connection to the proxy peer, collapsing per-IP limits, abuse metrics, and logs onto localhost. Blindly trusting forwarding headers would let direct clients spoof the same controls. Add an opt-in CIDR trust boundary and resolve X-Forwarded-For, Forwarded, or X-Real-IP only when the TCP peer is trusted. Walk proxy chains from the peer inward, stop at the first untrusted hop, and fall back to the peer on malformed input. Feed the resolved address consistently into rust-nostr connection policy, connection metrics, and lifecycle logs. Expose the setting across CLI/environment, NixOS, examples, and reference documentation. Multi-hop correctness assumes every trusted proxy appends or overwrites the forwarding chain and the backend is unreachable from untrusted networks. HTTP Git request accounting and PROXY protocol support remain out of scope. Validation: - nine address-resolution cases and three configuration cases passed; - `cargo fmt --check` passed; - `cargo clippy --locked --lib -- -D warnings` passed; - deployed first as exact commit `91b43bc157e459101f8011dba7f99eebb5a5e1b3`, then retained beneath the peer-state audit tip; - production logs retain `peer_ip=127.0.0.1` while reporting distinct forwarded IPv4 and IPv6 client addresses, demonstrating that connection accounting reaches the actual client; - more than 35 minutes of stacked production operation recorded zero service restarts and stable memory. Recommendation: ready to merge after the bounded-metrics proposal. |
||
|
|
9509d77bb0 |
Merge #b6f9e93b: fix(metrics): bound naughty-list cardinality
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqstd70f8wl7lrgsgp9pph060kpsjjudxzl6fzpjcd4lyhrl46ep9jc8gjfd4 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Production exposed one Prometheus series for every quarantined target and raw failure reason. Both labels are remotely influenced, and twelve-hour retention lets arbitrary peers create unbounded time-series churn while also copying their text into the metrics surface. Remove the per-target metric and retain the existing three-category aggregate unchanged. Regression coverage repeatedly rotates arbitrary relay URLs and reasons, proving the exported series remain fixed and peer text never reaches Prometheus output; the changelog calls out the compatibility impact for dashboard operators. Validation: - focused cardinality regression passed; - `cargo fmt --check` passed; - `cargo clippy --locked --lib -- -D warnings` passed; - deployed first as exact commit `a9a819801f23eaac16a1a748855a74a6d0053371`, then retained beneath two successive stacked production tips; - production exports only the fixed-category aggregate: the removed `ngit_sync_naughty_relay_info` series remains absent; - more than 40 minutes of stacked production operation recorded zero service restarts and no proposal-related warning. Recommendation: ready to merge before its dependent proposals. |
||
|
|
47e9c9a818 |
Merge #023b8804: feat(sync): cover roots with missing relay lists
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsqywugq3lzs4t8ddnzd80tw6hznnfdwzqj3femj9aq2yw4kg9pdmcax44v6 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: ## Problem Accepted root authors without a discoverable NIP-65 kind `10002` leave their conversations outside Sync+ even after a user-index relay has answered successfully. ## Approach After a successful empty index result, map only that author’s accepted roots onto a bounded operator-configured fallback relay set. The overlay uses the existing GRASP-02 historic, live, rotating, subscription-budget, and recovery machinery. A later accepted relay list removes the author from desired fallback coverage while shared subscriptions drain naturally. Failed index queries do not trigger fallback, and response-author expansion remains excluded. ## Validation before deployment - End-to-end scenario passes: empty index lookup activates one fallback root and recovers its reply; a later kind `10002` moves coverage to its declared inbox and removes fallback desire. - Discovery unit tests: 11/11 passed. - Configuration fallback test passed. - NixOS module parse passed. - Workspace clippy with warnings denied, rustfmt, and diff checks passed. - The broad test run passed all library suites and 95/96 Sync integration scenarios; the lone restart-burst proxy scenario passed standalone in 76.82 seconds and retains its documented load-sensitive baseline signature. ## Archive production validation Exact PR commit `e84be424462278516faa7c4dd95d403b58c0a9dd` ran on the disposable archive burn-in from 2026-08-12 17:50:24 UTC. - The service remained active with zero restarts; memory settled at 1.30 GiB peak. - Successful index batches exercised the intended missing-list case: fallback coverage grew from 30 to 151 accepted root authors, then stabilized while ordinary NIP-65 discovery continued. - The overlay added roughly 900–950 accepted roots to each of the four configured fallback relays. Existing filter consolidation used one additional live group where capacity remained and reused partial groups for later increments. - None of the four fallback relays emitted a rate-limit refusal. Existing unrelated low-capacity relays continued through the already-bounded cooldown/retry path. - No panic, OOM, killed process, or service restart occurred. ## Recommendation Ready to merge. The production burn-in demonstrates the previously uncovered population exists at meaningful scale, is covered through the existing bounded scheduler, and does not introduce a new failure or resource trend. |
||
|
|
6475e8e25f |
Merge #2cb6b021: feat(sync): make Sync+ operator-controllable
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqszed4syy49sax7fdld4y25l4svrltxkd7gh5h9dh9cwkns058f3rcezp77e PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: ## Outcome Add a default-on `NGIT_SYNC_PLUS_ENABLED` opt-out across CLI/environment, NixOS, examples, and reference documentation. The switch gates only GRASP-03 mailbox discovery; ordinary GRASP-02 proactive sync is unchanged. NIP-11 advertises GRASP-03 only while the overlay is enabled. This proposal is stacked on nostr:nevent1qqsrhrv4efjjrwgkct52xd0qa0rz24s4jmns6gznl9wv45a8xn8f9dgpz3mhxue69uhhyetvv9ujumn8d96zuer9wcn2g594 so it inherits the restored CI baseline. ## Validation - Sync+ configuration tests: 2/2 passed. - NIP-11 tests: 12/12 passed. - `cargo clippy --locked --workspace --all-targets -- -D warnings`: passed on the combined stack. - `cargo fmt --all -- --check`: passed. - NixOS module parse and `git diff --check`: passed. |
||
|
|
c38b7b8f24 |
Merge #3b8d95ca: fix: restore CI and auxiliary sync coverage
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsrhrv4efjjrwgkct52xd0qa0rz24s4jmns6gznl9wv45a8xn8f9dgzrwsj3 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: ## Outcome Restore all CI gates and repair the auxiliary live-coverage regression exposed while doing so. Full repositories without accepted roots once again enter bounded auxiliary maintenance, restoring `q`-tagged delivery after priority demotion. ## Validation - `cargo test --locked`: passed; sync 95 passed, 0 failed, 1 ignored, and every other unit, integration, and doc-test target passed. - `cargo clippy --locked --workspace --all-targets -- -D warnings`: passed. - `cargo fmt --all -- --check`: passed. - `git diff --check`: passed. The commit sequence keeps mechanical formatting, stale test contracts, observable readiness, the runtime coverage fix, and lint cleanup independently reviewable. |
||
|
|
5dfa954265 |
Merge #7e850008: feat(sync): discover conversations through NIP-65 mail…
feat(sync): discover conversations through NIP-65 mailboxes nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs8apgqpp9pay0yr20l3ev24n39x65penwld92f0a595jpqk3te4rcld7rej PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: # Recover accepted conversations through NIP-65 inbox relays ## Production problem GRASP follows repository relay ownership and accepted root references, but a participant may publish replies or updates only to inbox relays advertised in their NIP-65 relay list. Without bounded identity and inbox discovery, an accepted conversation can remain incomplete even though its author published enough information for another client to find it. This deliberately narrow proposal builds on the merged bounded repository-fetch work from nostr:nevent1qqsxe24pvp4pqxuzvpvdrdpgx3j2ug9yqtl6ny8z8mjw5k7djmludxspz3mhxue69uhhyetvv9ujumn8d96zuer9wcz87vs2. ## Approach - Limit identity discovery to owners and declared maintainers of accepted Full announcements plus authors of accepted roots. StateOnly/purgatory repositories and unrelated retained authors remain inert. - Query configured user-index relays for accepted authors missing kind 10002, requesting kind 0 alongside it in batches of at most 100. Once a list exists, follow its write/unmarked outboxes additively for newer replacements. - Admit, retain, broadcast, and serve identity events through the normal policy path. Only a successfully accepted/stored relay list may steer sync. - Rebuild inbox ownership from retained kind 10002 events before network refresh, so restart does not make users depend on the index source remaining online. - Feed inbox roots into the existing tiered descendant live, historic, and rotating REQ+EOSE machinery. There is no recursive author expansion or parallel data plane. - Keep discovery globally single-flight, ledger- and pacer-accounted, and require immediate transient capacity before starting it. Successful discovery refreshes after 24 hours; incomplete discovery retries after five minutes. - Apply replacement/removal to desired work immediately but allow physical live coverage to retire at natural EOSE/CLOSED/disconnect or ordinary consolidation. Runtime accepted-root deletion remains restart-reconciled because the self-subscriber has no bounded deletion signal. - Treat a relay reached only for NIP-65 discovery as a discovery-only connection: it shares ordinary authorization, pacing, and subscription-ledger safety, but does not start repository/announcement sync merely because it served identity discovery. One such source is dialled per maintenance pass, drained promptly, and promoted cleanly if it later becomes an actual repository source. The older broad proactive-sync proposal (nostr:nevent1qqszdapt0kvrhzgskxyqqu7aaudlt3s0cjj5v2t9fnky9v4cws2gahgw9lqz4) also proposed recursive scopes, response-author expansion, fallback relay configuration, and separate orchestration. Those pieces remain deliberately excluded. Simple is doing real work here. ## Reviewable commits - `1129d03` records the minimal design and exclusions. - `831eac2` derives accepted-root inbox overlays as pure indexed state. - `ced9706` adds bounded NIP-65 discovery through the existing sync pipeline. - `c8e49f2` bounds discovery, replacement, promotion, retry, and retirement. - `07fe32` makes identity admission and retention atomic and restart-safe. - `2538f90` exposes the three-relay user-index default through all four config surfaces. - `6fa84d5` changes replacement/removal to natural live-coverage retirement. - `870e125` expands discovery to accepted owners/maintainers and convergent NIP-65 outboxes. - `3917238` isolates integration sources so the end-to-end claim is meaningful. - `2f859b0` fixes a production-found lifecycle error by isolating discovery-only connections from ordinary fresh-start sync. ## Validation - `cargo test --lib`: 729 passed on the proposal before the final lifecycle refinement; `cargo check --lib` passes on exact tip `2f859b0`. - The focused proactive-sync integration passes on exact tip in 18.88 seconds. It covers identity admission, index discovery, outbox convergence, inbox-only reply recovery, StateOnly-to-Full promotion, natural retirement, and restart recovery with the discovery source offline. It also proves an advertised outbox used only for discovery never receives ordinary fresh-start sync. - The exact-tip full `--test sync --test-threads=1` run completed with 84 passed, 11 failed, and 1 ignored. A representative failure (`test_live_sync_layer3_events`) reproduces unchanged on exact merged parent `ff2280d`; the old live-sync/full-file failures are not hidden or expanded into this proposal. - The release Nix build completed and its library tests passed during the remote deployment build. ## Paired production burn-in Exact candidate `2f859b0` and exact merged parent `ff2280d` started from separate empty datasets at 2026-08-12 11:10:45 UTC. Both follow the same three original GRASP upstreams (`gitnostr.com`, `relay.ngit.dev`, and `git.shakespeare.diy`), run in the same background slice, have `MemoryHigh=8G` and `MemoryMax=12G`, and are scraped by Prometheus. Public gitnostr.com remained on its released package. The feature deliberately yielded to initial historic work. At 11:32:07 it activated its first accepted-author batch (43 authors, 4 inbox relays). Continued bounded rounds followed the accepted inventory as historic sync discovered it; by the 14:08 checkpoint the candidate retained 430 distinct kind-10002 authors, 472 profiles, and covered 338 inbox relays. The control intentionally retained no identity events because it has no Sync+ discovery path. A direct-ID attribution check avoided treating asynchronous total event counts as a feature delta. At 14:08 the candidate and control retained 26,066 and 26,384 unique events respectively, but their independent historic syncs had accepted different roots and those totals are not directly comparable. The candidate had 166 accepted-root descendants absent from the control. All 166 were queried directly from each of the three original upstreams in 10-ID REQs: 17 successful EOSE rounds per upstream, with zero failed rounds. **37 remained absent from all three original upstreams**: kinds 1 (5), 6 (1), 7 (8), 1111 (11), 1630 (1), 1632 (5), 1985 (2), 9735 (3), and 39701 (1). This is a strict point-in-time lower bound, not the total feature gain. It counts only descendants retained by the candidate, absent from the independently progressing control, and repeatedly unavailable by direct ID from the original source set. Events that Sync+ found earlier but the control later acquired are deliberately not claimed. Cold sync briefly filled the candidate's 1,000-event data lane around 11:34, with an 85.2-second worst queue delay while large new source batches arrived. Further finite discovery waves at 12:03 and 12:08 expanded coverage to 144 and then 198 inbox relays and produced another backlog (39.5-second worst queue delay, depth 256). This was material pressure, not hidden from the recommendation. It recovered without restart or failure: by 12:19 recent reports showed current depths mostly 0-31 and the most recent busy relay window was down to 4.34 seconds. The control also experienced cold-burn pressure on the shared constrained host. This evidence therefore claims bounded recovery after the full discovery frontier, not that extra coverage is free or improves baseline throughput. At the final 2-hour-57-minute checkpoint both archives had zero restarts and no panic, OOM, hard-memory-limit, or repository-corruption signature. Both exercised the 8 GiB soft boundary and correctly paused background Git fan-out; neither recorded a `MemoryMax` event. Candidate current/peak memory was 8.59/8.59 GB versus the control's 8.28/8.59 GB. Expected hostile-relay connection, signature, timeout, and rate-limit noise occurred in both instances. The three Prometheus targets, public gitnostr.com, caddy, and Prometheus remained healthy throughout. ## Deliberately shortened convergence measurement We stopped waiting for ecosystem-wide cold-sync convergence after 2 hours 57 minutes. Productive work was still flowing: in the preceding 15 minutes the candidate completed historic work on 26 relays and processed 3,000 events. Its eligible-author upper bound was 884, comprising stored root authors plus stored announcement owners/maintainers without externally reproducing the in-process accepted-Full filter; 430 had a retained relay list and at most 454 remained without one. Dead/unreachable relays are not treated as unfinished useful work: they back off to one low-cost probe per 24 hours. This cuts short the previously estimated remaining convergence wait by roughly 30 minutes to 2.5 hours. It does not cut short the stability soak: nearly three hours exercised multiple discovery waves, soft-memory pressure, historic pagination, and recovery. Global convergence is an unsuitable merge boundary because newly accepted repository inventory can legitimately extend the frontier while the service runs. ## Recommendation **Ready to merge at exact tip `2f859b0`.** The focused integration proves the bounded lifecycle, the paired production burn proves real incremental recovery, and the constrained archive survived the expanding frontier without restart, OOM, or hard-memory failure. Remaining uncertainty affects the eventual size of the benefit—not correctness or operational safety—and does not justify bloating this proposal or holding it open indefinitely. |
||
|
|
ff2280d9d5 |
Merge #87ff640e: fix(sync): bound peer-closed subscription recovery
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsg0lmype9p7dgaesvuadserhevmfu4we88herl6n2lgw37vc73urggazghv PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Production gitnostr.com on v2.1.2 (`bb38e6c`) accumulated 132,902 desired subscriptions for `nostr.wine` against a two-subscription baseline. Its RSS rose from roughly 4 GiB to 53 GiB before host-wide OOM pressure. Logs tied the growth to about 50 `auth-required` CLOSED responses per second. rust-nostr deliberately retains an `auth-required` subscription when an authenticator exists so it can answer the NIP-42 challenge and retry the same subscription ID. Our live-coverage actor independently generated replacement IDs, did not classify this refusal, and therefore left each rejected generation in the SDK registry. The first `auth-required` CLOSED now preserves the same SDK subscription and ledger slot for that NIP-42 retry. If the same subscription is refused again, ngit-grasp retires it from both the SDK registry and its ledger and applies the existing 24-hour policy pause. Every other peer CLOSED is terminally unregistered because SyncManager owns deliberate recovery. An otherwise unclassified live CLOSED enters the existing 65-second cooldown before one repair attempt instead of rebuilding immediately. The connection remains open throughout. This deliberately does not disable authentication or change rust-nostr globally. Correctness assumes a peer CLOSED is terminal except for rust-nostr's one same-ID NIP-42 retry, and that SyncManager remains the sole owner of replacement generations. Validation: - `cargo test --lib`: 711 passed, 0 failed. - Deployed exact tip `80086ceb9922ad2418215139bf95cde10030049b` to gitnostr.com and relay.ngit.dev. - gitnostr.com restarted from roughly 53 GiB RSS to about 1.5 GiB cgroup memory; relay.ngit.dev restarted to about 1.4 GiB. Both retain `MemoryHigh=32G` and `MemoryMax=48G` containment. - Both services remained active with zero restarts. gitnostr.com had no warning-level entries in the initial observation; relay.ngit.dev's warnings were bounded policy refusals rather than recovery churn. - relay.ngit.dev exercised the complete NIP-42 path in production: first `nostr.wine` refusals retained their same subscription IDs for authentication, a repeated refusal retired that ID and entered the 24-hour policy pause, and the former roughly 50-per-second replacement storm did not return. - relay.ngit.dev recorded zero repetitions of its prior `search filter is required` CLOSED storm. - Prometheus now scrapes cgroup CPU/memory metrics every 15 seconds on both hosts with 30-day, 2 GiB-bounded local retention, so renewed growth is historically queryable rather than dependent on point-in-time inspection. The archive candidate/control instances were disabled during incident recovery so they cannot compete with gitnostr.com while this exact tip is observed. |
||
|
|
9a9b3a0c58 |
Merge #6caaa160: Bound duplicate repository fetches and hedge slow sour…
Bound duplicate repository fetches and hedge slow sources nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsxe24pvp4pqxuzvpvdrdpgx3j2ug9yqtl6ny8z8mjw5k7djmludxsxzmeud PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: # Bound duplicate repository fetches and hedge slow Git sources ## Production problem The archive burn-in observed several simultaneous `git fetch`/`index-pack` pipelines writing into the same large bare repository. Existing controls were keyed by remote domain, protecting each upstream server from our request rate, and by aggregate cgroup pressure, protecting the host after pressure appears. Neither prevented independent identifier/domain workers duplicating expensive work against one local object database. ## Approach - Reserve per-domain fetch-pass capacity atomically and release it through an owned cancellation-safe permit. `ls-remote` remains inside this protection. - Coordinate work by resolved local repository path. Duplicate callers wait for the active wave and then re-check missing OIDs. - Give one preferred source a 30-second head start. If it remains incomplete, permit exactly one source on another domain to hedge; never start a third. - Make concurrent object ingestion free of `FETCH_HEAD` and automatic maintenance side effects, and serialize promotion of fetched OIDs. - Stream bounded Git stdout/stderr and expose aggregate activity, duration, outcome and byte metrics. Five minutes without either stream producing a byte terminates that command's private process group; there is no total duration limit, so an active large transfer may run indefinitely. - On inactivity, send the whole process group SIGTERM, retain its unreaped leader for a ten-second grace period, then SIGKILL before reaping. A guard applies the same group cleanup if the owning Rust future is cancelled. - After an unproductive primary/hedge pair has fully ended, try the remaining sources sequentially. A third fetch never overlaps the bounded pair. The two attempts deliberately do not produce a learned source ranking. They write into the same object database, so either command can observe an object installed by the other and completion order cannot attribute the object to a source. Truthful ranking would require isolated per-attempt object stores and explicit winner promotion, which is excluded here in favour of the smaller, correct hedge. The 30-second hedge delay is not a timeout. The separate five-minute inactivity limit measures transport silence rather than elapsed runtime; every output byte resets it. Timing remains operational evidence only and never ranks sources. ## Commits - `b9ed6e8` atomically reserves remote-domain capacity. - `3903ffe` serializes work per object database and rechecks waiting demand. - `665af5b` removes shared fetch-head and maintenance side effects. - `e1eee18` adds exactly one delayed distinct-domain hedge without scoring. - `0d7c77d` introduces bounded concurrent stream draining and operational metrics without changing completion policy. - `25c535f` adds inactivity recovery, cancellation-safe process-group cleanup, and remaining-source fallback. ## Validation - Full library suite: 709 passed. - Deterministic scenarios cover duplicate-demand single-flight, independent repositories, delayed hedge victory, two slow sources with no third attempt, and same-domain alternatives excluded from hedging. - Domain admission contention and cancellation tests pass. - Stream capture remains bounded while readers continue draining/counting. - Two real concurrent object-only Git fetches into one bare repository leave `FETCH_HEAD` absent, all requested objects readable, and `git fsck` clean. - Tests cover a productive delayed hedge after an empty primary and prove a shared-object observation cannot become source preference. - An active-output fixture survives a short test inactivity threshold. Silent TERM-resistant process groups are forcibly reaped; cancellation also kills descendants before follow-up work proceeds; an unproductive pair reaches a productive third source without three-way concurrency. ## Archive validation The earlier `dfa9afc` burn-in demonstrated the hedge and foreground health but also exposed that its persisted rankings were not attributable in a shared object database. That recommendation was withdrawn. Exact corrected tip `890321b867e8cf4765b1470b65c27b0ea4682526` ran on the constrained disposable archive from 2026-08-10 21:57:38 UTC. In the first 9 minutes 47 seconds it: - completed 325 repository Git passes and promoted 146 queued repositories; - exercised 97 successful hedge advertisements and 87 successful hedge batch fetches, so the delayed alternative path was active under real load; - recorded zero restarts, panics/OOMs, Git corruption signatures, or failed service health; - reached 3.50 GiB peak memory and settled to 19 tasks while the existing resource-pressure gate paused new fan-out ten times rather than disrupting foreground work; and - emitted one 60-second activity warning for a primary `relay.ngit.dev` batch that was still producing progress (`quiet_secs=0`, 20,835 stderr bytes). It was observed rather than killed, as intended for a potentially large legitimate fetch. That run remains useful threshold evidence: the only 60-second warning had `quiet_secs=0`, so the legitimate large fetch would not have approached the new five-minute inactivity boundary. It predates process-group recovery, however. Exact corrected tip `25c535f96aa9969543116a5c96a77aa5971130a6` then ran as the control in the paired constrained archive from 2026-08-12 01:53:19 UTC. By 03:11 UTC it had completed 1,785 Git passes and fetched 2,985 requested OIDs. The real workload exercised both sides of the hedge: 50 hedge batch fetches and 6 hedge residual fetches completed successfully, alongside 289 primary batch and 168 primary residual successes. All subprocess gauges returned to zero. Across that run the exact tip had zero service restarts, surviving zombie Git processes, panic/OOM, or repository-corruption signatures. Memory peaked at 4.76 GB and returned to 1.42 GB while the service remained responsive; its `/metrics` endpoint answered in 15.6 ms at the settled comparison. No five-minute inactivity termination was needed in this workload. Earlier threshold evidence had already shown a legitimate 60-second fetch continuing to produce output, and the final implementation correctly leaves such active work unbounded by total duration. The burn also exposed one pre-existing synchronous local-OID-copy path outside this proposal's observed network-fetch/process-group wrapper. A single zombie was temporarily pinned behind that caller during an earlier refinement run, did not accumulate, and cleared on the deliberate restart. It is not caused or masked here and remains excluded from this bounded network-fetch change. This exact-tip constrained production evidence, together with the deterministic process-group and fallback tests, supports merging this proposal. |
||
|
|
5c1fbb2fea |
Merge #75950a9a: Protect foreground service from cold-sync Git fan-out
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs8t9g2nggd9rsmshw2pumpefnw0rpavl2zpwjnsh7sy0sv9fvdnkgasgyjf PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: # Protect foreground service from cold-sync Git fan-out ## Production problem A clean archive burn-in was run under `CPUQuota=200%`, `MemoryHigh=12G`, and `MemoryMax=16G` from 2026-08-10 15:14:20 UTC. Per-domain purgatory throttles were individually respected, but a cold sync spanning many domains created roughly 149 service tasks, dominated by Git subprocess pipelines. Against the same ngit-grasp process during that workload: - idle NIP-11 latency of about 0.3 ms rose repeatedly to 1-5+ seconds; - several NIP-11 requests timed out after five seconds; - WebSocket connection/query probes intermittently failed; - two-second Prometheus scrapes timed out; and - cgroup CPU throttling accumulated continuously. This demonstrates actual foreground starvation. The existing per-domain limit protects each remote server, but does not bound aggregate work across many different domains. ## Approach Place complete event-directed background Git passes behind a small resource-pressure gate: - The first one-second observation window admits two passes per effective CPU, discovered from Linux cgroup v2 `cpu.max` or available processor count. - Each healthy observation window grows the allowance by 20% or one pass, whichever is greater, so there is no permanent aggregate limit on an unconstrained machine. - New cgroup CPU throttling or memory-high pressure reduces new admission to one pass while already-running work drains naturally. - Sustained pressure is logged at most once per minute, keeping the operational signal without flooding logs as healthy and throttled samples alternate. - Existing per-domain concurrency and request-rate protections remain intact. - Foreground Git pushes do not use this outbound recovery path. This deliberately avoids configuration, a permanent concurrency ceiling, and a general-purpose priority scheduler. ### Why ramp before pressure exists The original implementation had no aggregate limit: its per-domain limits could collectively launch work across hundreds of independent domains. A fixed aggregate ceiling would solve that burst but needlessly constrain hosts with ample resources. Waiting for pressure before applying any restraint also proved too late: cgroup CPU and memory signals are retrospective, so roughly 149 tasks had already escaped before the kernel could report their cost, and they had to drain before foreground service recovered. The initial allowance is therefore a one-second discovery probe rather than a lasting limit. Healthy observations increase it without a ceiling; pressure only pauses new admission while existing work drains. Growth is deliberately 20% or one pass rather than strict doubling: `4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 12 ...` gives each preceding step time to appear in the next pressure sample, while an unconstrained host continues opening capacity. For best-effort background recovery, a few seconds of slower capacity discovery is preferable to risking foreground relay availability. ## Validation before production comparison - `nix develop -c cargo test --lib`: 692 passed before the final isolated ramp-growth refinement. - Exact-tip throttle tests: 14 passed, including gradual unbounded growth. - Throttle-focused tests cover the initial probe, CPU-quota parsing, pressure detection, and existing per-domain behavior. - Targeted rustfmt and `git diff --check` pass. ## Production validation The strict-doubling predecessor `7c16ff9` first completed a clean, roughly ten-minute burn-in under the same two-CPU, 12/16-GiB constraints. All 23 sampled NIP-11 requests and all Prometheus scrapes succeeded; task fan-out stayed at 24-55 rather than the baseline's roughly 149; memory peaked at 5.23 GiB; and the service completed 344 Git passes while genuinely CPU-throttled. There were no restarts, panics, allocation failures, or OOM events. Exact final revision `434e43fc6240be24957bf876bedf6db10c919ed5` replaces doubling with 20%/+1 growth. It was deployed only to the disposable archive, whose dataset was deleted again before a clean burn-in from 2026-08-10 19:36:55 UTC. Through 19:40:54 UTC: - all 10 sampled NIP-11 requests succeeded in 0.0003-0.37 s and all sampled Prometheus scrapes succeeded in 0.012-0.60 s; - WebSocket REQ/EOSE completed successfully in 1.87 s; - service task fan-out stayed at 12-34 and memory peaked at 3.11 GiB; - 1,799 of 2,410 CPU periods were throttled, exercising the pressure path; - the process nevertheless completed 64 Git passes and fetched 645 requested OIDs, reaching 2,320 repositories and a 1.8 GiB archive dataset; and - there were zero memory-pressure/OOM events, restarts, panics, or allocation failures. Four pressure warnings over four minutes confirmed the one-per-minute bound. The candidate therefore preserves foreground relay operation under the same cold-sync pressure that reproduced starvation, while continuing to make substantial sync progress. This exact revision is recommended for merge. |
||
|
|
5bad62375b |
Merge #347f6831: sync: tier live coverage under connection pressure
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsrglmgx8q2u69zk6zppglyvn9q45cfv5juy2yv7khq0a23xlpl5hg54dczz PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Reference coverage now has ordered capacity tiers instead of a binary core/descendant split. Announcements, state, canonical repository `a`, and canonical root `e` coverage remain essential; root `E`, core `q`/`A`, descendant `e`/`a`, and descendant `q` are admitted as complete priority tiers when the shared NIP-11-aware ledger has room. Descendant `E`/`A` and every tier below the cutoff use the existing cursor-overlapped REQ+EOSE rotation. The four commits are deliberately reviewable in isolation: 1. Name and test the ordered filter vocabulary without changing wire behaviour. 2. Apply the complete-tier live cutoff and paced packed fallback through the existing ledger and five-second scheduler. 3. Union core and descendant reference values before historic grouping, reducing overlapping delivery without changing coverage. 4. Preserve the complete auxiliary frontier when any admitted live subscription receives CLOSED, avoiding a reconciliation-window gap. ## Validation - `nix develop -c cargo test --lib`: 686 passed before the final focused lifecycle assertion; the final tip's focused lifecycle test also passes. - All three descendant live/fallback/addressable integration scenarios pass together. - All three adaptive-pagination scenarios pass. - `nix build .#ngit-grasp` passes; the final small lifecycle commit was then built successfully by the archive's remote Nix deployment. - A full `--test sync` run retained timing failures that reproduce on master under the already-merged per-relay query pacer. The branch-specific descendant failures exposed by that run were fixed by rotating one relay-compatible packed REQ rather than singleton filters. ## Archive production evidence Exact proposal tip `27f31e50ed86ce83f12bd247c4bb7be8b8cbf2b0` was deployed to the disposable archive burn-in instance at 10:13:51 UTC on 2026-08-10 and observed through 10:41:48 UTC. The service remained active with the original PID and zero restarts. Memory was 611 MiB current / 613 MiB peak at the final trend sample while archive hydration was still active. The archive exercised both sides of the policy across real relay sizes and advertised capacities: - `git.shakespeare.diy` (fallback budget 20): 4,587 event IDs plus 5 coordinates; 36 filters stayed live in 14 subscriptions and 11 filters became 5 packed rotating REQs. - `nos.lol` (advertised budget 20): 2,636 event IDs plus 12 coordinates; 24 filters stayed live in 8 subscriptions and 7 filters became 3 packed rotating REQs. - `relay.primal.net` (advertised budget 20): 850 event IDs plus 3 coordinates; 16 filters stayed live in 5 subscriptions and 3 filters became one rotating REQ. - `relay.kcbitcoiners.com` (advertised budget 2,000): ordinary auxiliary tiers fit live; only the deliberately historic-only tier rotated. Across the window, 17 distinct relays exercised persistent tier admission and 17 exercised fallback rotation. The service started 457 packed fallback batches and confirmed 472 descendant-purpose batches. Five early confirmations inherited a broader batch failure flag but each corresponding cursor reached terminal success and later cycles continued; no auxiliary live subscription received CLOSED. There were zero query-rate-limit responses, zero subscription-limit responses, zero panics/OOM signatures, and no unexplained auxiliary-coverage retirement (the sole retirement followed a real frontier change and was reinstalled). The observed watchdogs were two pre-existing empty historic pages from `relay.nostrcheck.me`; neither involved the new descendant rotation. Four policy refusals came from the access-restricted `relay.getalby.com/v1` and followed the existing 24-hour deferral path. External authentication failures and peer resets remained isolated to their established recovery paths. ## Recommendation Ready to merge. The archive demonstrated live and rotating coverage concurrently across fallback and advertised capacities from 20 to 2,000, with packed multi-filter rotation completing repeated full cursor laps on the largest relays. The remaining remote failures are pre-existing peer policy, authentication, transport, and historic-page recovery cases rather than regressions in this proposal. |
||
|
|
d318226849 |
Merge #ba2d248c: feat(sync): follow addressable descendant references
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqst5tfy3s2fsrz8psulwwgs72klm9d3jyxhwnxzkjdmdjpf979jfyqhs0akd PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Repository descendants can refer to a direct thread member by its NIP-01 address rather than its event ID. The existing non-recursive frontier retained only event IDs, so `a`/`A` tags and coordinate-valued `q` tags could fall outside both live and historic coverage. This represents the frontier as event IDs plus coordinates derived from valid replaceable and addressable direct members. Both sets use the existing live admission, five-second fallback rotation, and ordinary historic sync paths. Malformed addressable events without `d` do not contribute a coordinate. The one-generation boundary and existing connection-budget policy are preserved. Recursive discovery and connection sharding remain deliberately excluded. ## Validation - `cargo test --lib`: 682 passed. - All three descendant integration scenarios pass together, covering retained live mode, constrained event-ID fallback, and constrained `a`/`A`/coordinate-`q` fallback. The constrained fixtures advertise four subscriptions so auxiliary live coverage cannot legitimately fit. - The final proposal commit is `533cb6279d2197d624e2d6b622e8abba86ecc56d`. ## Archive production evidence The runtime change soaked on the disposable archive instance from 08:38:00 to 08:57:33 UTC on 2026-08-10. Later amendments changed only the test capacity and commit prose; the runtime tree was unchanged. The archive pin now names the final proposal commit, running from `/nix/store/nb0vw0im8xp4x61xzhdymh88rfn8wwmy-ngit-grasp-2.1.2` since 09:02:44 UTC. Real addressable frontiers were retained within the shared subscription ledger: - `nos.lol`: 2,620 event IDs plus 11 coordinates, 21 filters in 8 subscriptions. - `relay.primal.net`: 840 event IDs plus 2 coordinates, 9 filters in 3 subscriptions. - `nostr.bitcoiner.social`: 166 event IDs plus 7 coordinates, 6 filters in 1 subscription. - `ngit.danconwaydev.com`: 2,547 event IDs plus 34 coordinates, 21 filters in 8 subscriptions. - `relay.mostr.pub`: 143 event IDs plus 1 coordinate, 6 filters in 1 subscription. Across the evidence window there were zero query-rate-limit signatures, zero watchdog signatures, zero relay-disconnect logs, and zero panic/OOM signatures. The service did not restart. Existing unrelated remote-relay hydration and policy failures remained bounded by their established recovery paths. The constrained fallback was proven deterministically by the integration scenario; the archive window exercised live coordinate coverage but did not naturally reach a coordinate-bearing constrained relay before the final provenance redeploy. ## Recommendation Ready to merge. The implementation extends the existing bounded mechanisms rather than adding another scheduler, subscription class, or connection. |
||
|
|
e61bee6f58 |
Merge #599cc928: sync: minimise live subscription churn
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs9n8xf9rnczwhv6u6yagtp95u8y45fd2pahtthkfensw5nnaekx0sls8puq PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Built on the descendant-sync commits merged by #2b377e20 and implements nostr:nevent1qqswhdur5f7z8zwy726za854qhahnt9k5ytru2h6zqyysrzdskwzk9cpz3mhxue69uhhyetvv9ujumn8d96zuer9wcykq3rj. The proposal and current master share exact descendant tip `9f3c943`, so it can merge without rebasing or duplicating commits. Repository discovery is batched every five seconds. Previously each batch added live subscriptions until a coarse threshold rebuilt the complete live set, needlessly closing healthy core and descendant subscriptions. This proposal is split for review: 1. `186d9a8` makes repository filter chunking deterministic, giving stable grouping independent of HashMap insertion order. 2. `9a1fa8d` uses the real filter-count and serialized-byte grouping result to select useful consolidation work. Full core and descendant groups remain open. If repacking the complete mutable tail releases slots, it does so; if byte limits mean the tail cannot shrink, it replaces only the smallest useful subset needed to reduce additional slot usage. Failed CLOSE or replacement-open operations restore the exact retired tail. Capacity pressure retains the existing deferred full-regroup backstop while historic recovery continues. Validation: - `cargo check --lib` - `cargo test --lib`: 680 passed - tests cover stable full/auxiliary IDs against a real embedded relay, deterministic grouping, count and byte constraints, replacement-open rollback, and partial-CLOSE rollback - 13 small partial groups plus one new filter become 2 replacements, releasing 11 slots - 17 byte-bound groups plus one small filter replace only 1 group, avoiding a new slot without rebuilding the other 16 - a byte-full group which cannot help is left untouched Disposable archive production refinement: - the count-only version exposed the exact byte-bound case: a one-filter update on relay.ngit.dev retired 17 groups and reopened 17, releasing no slots - exact final tip `9a1fa8d`, under the equivalent roughly 50,000-ID / 167-chunk startup workload, preserved 17 groups and replaced exactly 1 for the same one-filter update, with zero additional slots - the final-tip service remained active with zero restarts, zero candidate-specific rate-limit, ledger-overrun, rollback, or panic errors, and about 349 MB peak during the comparison window - the public gitnostr.com service remained on released v2.1.2 throughout Earlier archive evidence also established that descendant permanent coverage, multi-subscription splitting, constrained fallback rotation, and historic completion continue correctly beneath this change. Recommendation: ready to merge. |
||
|
|
b9eebb2e25 |
Merge #2b377e20: feat(sync): schedule direct-member descendant history
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqszkdm7yq5lkte4ycw72r3esglyp5z2g70cdqm0gv9zkfgyrttk2kq0su8wr PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Closes #727b0992 with the capacity-aware hybrid design agreed in review. Repository collaboration events may reference only their immediate parent. Existing repository/root filters can therefore miss replies to a discovered reply when those events omit repository and root tags. The proposal is intentionally split into four reviewable commits: - `d203f9d` refactors the existing historic REQ path around one shared queued submission helper, without changing scheduling. - `5f84b8b` includes known direct-member descendants in ordinary historic coverage. - `4dced40` retains separately consolidated descendant live coverage when core live coverage, a two-slot control reserve, and at least one transient historic slot all fit the relay per-connection budget. - `9f3c943` otherwise rotates descendant filters through the same transient ledger, starting at most one queued or active descendant request per relay, on a five-second cadence. Fallback requests use REQ+EOSE, advance their cursor only after successful EOSE, and overlap the prior successful upper bound by 15 minutes. They do not skip a filter merely because the transient slot is busy: the request waits in the existing queue. Permanent coverage is paired with the complete historic baseline so events created before installation are not missed. The design deliberately does not add request-class priority, recursive descendant traversal, durable cursors, multi-connection sharding, new configuration, or minimum-churn core consolidation. The latter is tracked separately at nostr:nevent1qqswhdur5f7z8zwy726za854qhahnt9k5ytru2h6zqyysrzdskwzk9cpz3mhxue69uhhyetvv9ujumn8d96zuer9wcykq3rj. Local validation: - `cargo check --lib`: passed - `cargo test --lib`: 673 passed - forced-fallback integration at advertised capacity 5: passed - permanent-live integration: passed - 600-member unit scenario confirms descendant filters split across multiple live subscriptions - full `cargo test --test sync`: 83 passed, 10 failed, 1 ignored; failures retain established load-sensitive signatures while both new descendant scenarios pass under whole-file load Disposable archive production evidence: - exact tip `9f3c943` and the behavior-identical pre-amend executable both started with zero restarts and no candidate-specific rate-limit, panic, or ledger-overrun errors - dependency sync queued 2,293 IDs in 8 chunks and 34,868 IDs in 117 chunks without starving later work - permanent descendant coverage was installed across real relays; examples include 24 filters split over 12 subscriptions on relay.damus.io, 9 over 3 on grasp.budabit.club, and 6 over 2 on git.nostrhub.io - the naturally constrained git.shakespeare.diy session had a 20-slot fallback budget; 27 descendant filters could not fit alongside 1,279 repositories and 961 roots, so filters 0, 1, and 2 rotated sequentially and completed successfully without skipping the busy transient queue - the refined exact tip remained stable with bounded memory (about 517 MB peak during its observed window) - the stacked follow-up executable retains the same descendant behavior and is now exercising a 49,253-ID dependency batch on the same archive dataset without restart or rate-limit failures Recommendation: ready to merge. |
||
|
|
7b5b81ff14 |
Merge #a44ff1d9: fix(sync): adapt query starts after rate-limit exhaust…
fix(sync): adapt query starts after rate-limit exhaustion nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs2gnl3m9fv4zvge4mfc0rzfgpgfe7l4zemrr62crwsvfjcdp5u9uq9y99ms PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Production showed a roughly 67-second oscillator against relay.ngit.dev: simultaneous subscriptions stayed bounded, but query turnover exhausted a separate 120-query-per-minute allowance after every cooldown. That allowance is a newly enabled rust-nostr 0.45 LocalRelay default, not an ecosystem floor: the preserved cross-relay survey in commit |
||
|
|
50033e0157 |
Merge #97556042: chore(sync): diagnose missing transient terminals
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsfw4tqgfrz874nyp8ae9h08dsrj0zc44w7p5qcgc5xgx7m5epu4rctceh6k PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Production at `58ef8682` repeatedly held transient subscriptions until the 120-second watchdog across independent relays and request classes. Diagnostic tip `b55d2de` was deployed to gitnostr.com at 2026-08-07 14:52:34 UTC against a directly comparable startup workload. The diagnostics established that expired subscriptions were already absent from rust-nostr's active subscription map, so the relay terminal had arrived but ngit-grasp had not released its local ledger ownership. An initial 4,096-message queue removed watchdogs from a 36,054-ID relay.ngit.dev burst, but production falsified queue sizing as a general solution: the five-per-cycle signature moved to ngit.danconwaydev.com and then git.shakespeare.diy, including four empty responses on each peer. Empty responses exposed a second ordering hole, while the shifting high-volume peer showed that any finite EVENT queue can remain congested under sustained pages. The final proposal fixes both lifecycle dependencies by construction: - register a second rust-nostr broadcast receiver synchronously as a terminal-control lane; it consumes EOSE/CLOSED and connection terminal status independently of the processor-facing EVENT queue; - pre-generate every transient subscription ID and register its generation-scoped permit before sending the REQ, pass that ID to rust-nostr, and roll registration back on subscribe failure. The terminal listener is the sole transient-release path during a connected session. The processor listener still delivers ordered EVENTs, pagination terminals, and live CLOSED restoration. EOSE still enqueues CLOSE before returning the slot; CLOSED and connection teardown remain definitive boundaries. The EVENT queue stays at its original bounded 1,000 messages—correctness no longer depends on buying lifecycle headroom with memory. Subscription concurrency, filter contents, pagination, watchdog timing, retry behaviour, live lifecycle, and the configuration surface are unchanged. The bounded diagnostic fields remain useful for attributing any residual watchdog. Regression coverage deliberately blocks a capacity-one EVENT lane behind 1,200 LocalRelay events and requires all transient permits to release within three seconds. A second scenario opens 25 immediate empty queries and requires every pre-registered permit to release within the same bounded deadline. The architecture document records the implemented control/data separation and registration ordering. Pre-deployment validation of final tip `04b93c9`: - all 648 library tests pass; - both focused lifecycle regressions pass; - standalone `startup_historic_sync_stays_within_relay_req_concurrency_limit` passes with zero rejected REQs; - changed-source rustfmt and `git diff --check` pass. Production verification of exact tip `04b93c9a1df9d28fc6af96b131d0ebc5646552ca`: - remotely built and activated on gitnostr.com at 2026-08-07 15:37:54 UTC; - immediately reproduced a larger comparable workload: relay.ngit.dev had 994 missing IDs/4 chunks and 38,518 IDs/129 chunks; - through 15:45:27 (7m33s, more than three complete 120-second recurrence windows), there were zero watchdogs with `sdk_subscription_active=false`, including zero at relay.ngit.dev, ngit.danconwaydev.com, and git.shakespeare.diy; - four watchdogs did fire correctly for genuinely active relay-side subscriptions: three historic pages at relay.cyberguy.fyi and one at nostr.sebastix.dev, all `sdk_subscription_active=true`; - the target relay continued useful progress: its generic announcement batch completed and later REQ/EOSE batches had confirmed 86 state-only repositories by 15:40:45 despite the pre-existing startup rate-limit CLOSED burst; - service remained active with zero restarts, zero panics, zero budget-exhaustion/concurrency-limit messages, and 1.24 GiB memory at the final sample. This exact tip is recommended for merge. Production distinguishes the repaired local-accounting defect from the watchdog's intended recovery of peers that genuinely leave subscriptions open. |
||
|
|
32813b92f1 |
Merge #a36a8dc2: chore(sync): classify transient watchdog recovery
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs2x65dcgs4h5flnnjyj945khwrq6dt3tlnet5t9tut4wtfyc2ej5ggjhke8 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Production at `57bc76ea` forced exactly three `relay.ngit.dev` transient subscriptions through the 120-second watchdog every two minutes: 30 CLOSEs in the first 20 minutes. Subscription accounting remained within bounds, but the held-permit state and warning retained only random subscription IDs, so the stalled lifecycle could not be distinguished without guessing. Require every auto-close REQ caller to provide one of six bounded request classes, retain that class with its per-session permit, and expose it in watchdog logs and a class/outcome Prometheus counter. Pagination distinguishes ordinary continuation from NIP-11 hint verification using its existing state; exact-ID purgatory polling remains correctly outside these labels because it uses the separately timed `fetch_events` path. This is observability only: timeout, retry, pagination, request admission, filter contents, and slot-release behaviour are unchanged. Relay URLs and subscription IDs are deliberately excluded from metric labels, and no configuration surface is added. Validated by the 646-test library suite, including fixed-label uniqueness and hint-verification classification tests, plus rustfmt and `git diff --check`. Production verification on gitnostr.com deployed from exact PR commit `58ef8682f3da6447f8fd52560f6148fe3ba4e6a9` from 2026-08-07 13:49:09 UTC: - `ngit_build_info` reports commit `58ef8682f3da6447f8fd52560f6148fe3ba4e6a9`. - The service remained active with zero restarts and no error-level journal entries. - Nine watchdog recoveries across three complete 120-second cycles were all labelled `request_class="negentropy_hydration"`, all targeted `wss://relay.ngit.dev`, and all successfully sent CLOSE; the metric recorded the same class/outcome. - Startup negentropy advertised 995 and 40,071 missing event IDs, split into 4 and 134 hydration chunks. The three-at-a-time cadence is therefore queued hydration work advancing as three stalled relay-side subscriptions are closed, not unbounded retry or pagination traffic. The change has produced the intended low-cardinality diagnosis without changing request behaviour. It is ready to merge. The remaining missing-EOSE behaviour should be reassessed after the still-old `relay.ngit.dev` is upgraded, rather than speculatively changing the client in this PR. |
||
|
|
64f33dcda7 |
Merge #843ee3ad: fix(build): publish the deployed source revision
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsgg0hr45szmdeum8mahjkvzvfvmfndpgtayxtl9jhqsfajtyhys6gc8japx
PR-Author: DanConwayDev's Agent
nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0
PR description:
Production currently exports ngit_build_info{commit="unknown"}, so every stabilisation cycle must reconstruct the running revision from the nixos-fi1 pin, Nix store path, and activation time. Two independent gaps cause this: metrics reads GIT_HASH while build.rs emits GIT_COMMIT_SHORT, and filtered Nix source archives intentionally contain no .git directory.
This change gives all build surfaces one trustworthy revision source. Nix package and NixOS-module outputs inject the locked flake self.rev; ordinary Cargo builds retain a Git HEAD fallback. Metrics publish the full 40-character revision, while NIP-11 and the landing page retain an eight-character display form. Direct imports of nix/module.nix remain supported and report unknown unless a revision is explicitly supplied.
Validation: 21 metrics-related library tests passed; nix flake check --no-build passed for packages and both exported modules; clean-flake evaluation equalled the committed revision; nix build .#ngit-grasp passed; and the resulting binary contains both the exact full revision and expected short display revision. Production verification will confirm the metric, NIP-11 document, and landing page agree with this PR tip.
|
||
|
|
87d7732910 |
Merge #86376f5d: fix(relay): preserve bursty client sessions
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsgvdm0t5fnvz2q5zf2p6myctssejzp8t6h5cq9fz84qrkv8u784sc3dgkww PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: ## Production problem rust-nostr 0.45 introduced a connection-wide 300-frame-per-minute bucket and closes the WebSocket when it is exhausted. gitnostr.com recorded 5,020 `too many client messages` disconnects between 2026-08-06 10:00 and 2026-08-07 07:55 UTC. This predates and is independent of the subscription-budget ledger. Correlating relay timestamps with Caddy's real-client access log showed two populations: - a rapid source sent more than 300 frames and roughly 1.2 MiB in about 1.5 seconds, which should remain bounded; - legitimate browser sessions from `gitworkshop.dev`, `gittr.space`, `armada.buzz`, and a localhost development origin also crossed the ceiling. ## Approach Raise only the fixed connection-wide allowance to 6,000 messages/minute (100/second). All activity for a relay is multiplexed through one gitworkshop WebSocket: independent loaders, pagination, reconnects, publishes, AUTH, REQs, and CLOSEs. Batching occurs within individual loaders rather than across the whole connection. An initial 1,200/minute production candidate was deliberately rejected after it disconnected a legitimate localhost browser session sustaining roughly 44–45 frames/second. The final allowance provides about 2.2x sustained headroom over that observed workload, plus the token bucket's initial burst capacity, while retaining a finite catch-all for malformed or non-operation frames. The tighter independent quotas remain unchanged: 60 EVENT writes, 120 queries, and 30 authentication events per minute. No new configuration option is added because this non-standard limit cannot be discovered by clients and ngit sync cannot adapt to it. ## Correctness evidence - A 1,201-frame burst keeps the WebSocket usable and completes a subsequent REQ/EOSE exchange. - A rapid 6,001-frame burst is still disconnected. - `nix develop -c cargo test --test relay_message_rate`: 46 passed. - `nix develop -c cargo test --lib`: 643 passed on the initial candidate; the final change only raises the tested constant and the final targeted suite passed 46/46. - `nix build .#ngit-grasp`: passed on the initial candidate; deployment built the exact final package. ## Production validation The exact final tip `a6b5e3a9f66f8bb6e4cc3f23ef62c9be1a9b37f2` was deployed as `/nix/store/bp02cmfviwmf03px52xbjqs8rk4a01ih-ngit-grasp-2.0.0` at 2026-08-07 09:43:58 UTC. Through 09:57:33 UTC it admitted 874 WebSocket connections with: - zero `too many client messages` closures; - zero concurrent-REQ rejections, connection-permit rejections, or subscription-budget failures; - zero restarts and zero panics; - 18 expected transient-REQ watchdog CLOSE recoveries; - approximately 995.9 MB peak memory. This exact-tip production evidence, together with the failed lower-limit experiment and regression scenarios, supports merge. ## Excluded scope Per-IP admission fairness and changing rust-nostr's upstream default are separate concerns. A 6,000/minute upstream default is worth proposing as a pragmatic compatibility floor, but this PR does not alter upstream policy. |
||
|
|
f15b49c39e |
Merge #f1ed9f57: fix(sync): unify per-connection subscription budgets
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs0rmvl2u9pmpfct2zaxqc2p84n0jcsnxjrkhdnjxp93u4a00a0l0gstttec PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Root cause and scope: the standalone sixth-REQ failure was partly a test-proxy accounting defect—the relay had sent CLOSED, but the proxy retained dead subscription IDs. Commit `8d1aaeb` fixes that harness without making a production claim. Commit `5653aaa` then replaces separate NEG/REQ limits with one per-session ledger derived from NIP-11 `max_subscriptions`, falling back to 20 and reserving two slots. It accounts persistent live groups, NEG, historic/fallback/pagination/verification/retry REQs, and exact-ID purgatory polls. Live L1+L2+L3 coverage is transactionally replaced/restored; stale-session waiters are generation-rejected; live CLOSED triggers complete-set recovery. Multi-connection sharding remains out of scope. Timeout correction: production disproved the initial whole-session teardown approach. It caused avoidable reconnect and actor-backlog delays when relay.ngit.dev omitted EOSE/CLOSED. Commit `15369ca` instead sends CLOSE for only the timed-out subscription and releases only its generation-scoped slot after the SDK accepts CLOSE; a failed send retains the slot until ordinary connection teardown. This matches the accepted accounting boundary and removes 49 net lines from the ledger tip. Acceptance-fixture correction: an initial fifth whole-file run reported one rejection at a recorded peak of five, but the proxy did not retain the rejected ID. Review found no subscription-opening bypass and found that the proxy checked capacity before recognizing same-ID REQ replacement, while also treating EOSE as closure. Commit `2bdc11d` makes the fixture follow NIP-01: replacement does not add occupancy, EOSE retains it, and only CLOSE/CLOSED releases it; future rejections log the exact frame and active IDs. This is test-only and does not claim the uninstrumented rejection was conclusively a replacement. Review follow-up: `add9c66` corrects the architecture document to describe the implemented per-subscription watchdog CLOSE rather than the abandoned session teardown. `f5081a3` re-derives sync work after deferred live consolidation, preventing quiet relays from stalling until an unrelated trigger. `cb0af7b` admits transient work without waiting while holding only its class or ledger permit, and explicitly rolls back earlier live permits on a session-generation error. CLOSED-burst coalescing remains excluded: generation-only deduplication could suppress a legitimate CLOSED from replacement coverage and needs a separate lifecycle design. Validation: `cargo test --lib` passed 638/638; the standalone req-concurrency scenario passed under the stricter relay-lifetime proxy; `nix build .#ngit-grasp` passed. After the review fixes, five consecutive full `--test sync` runs passed in five attempts, including five req-concurrency passes and zero recorded proxy rejections. The final `e53c576` commit adds only the unreleased changelog entry, so it does not invalidate runtime evidence for parent `cb0af7b`. Production evidence (gitnostr.com, reviewed runtime `cb0af7b`, deployed 2026-08-06 23:21:21 UTC from nixos-fi1 `e0eb753`): across the exact 3h00m06s window ending 2026-08-07 02:21:27 UTC, 82 relay sessions configured ledgers and 125 stalled transient requests were recovered with individual CLOSEs. There were zero subscription-limit rejections, zero budget-exhaustion warnings, zero relay.ngit.dev disconnects, zero panics, and zero service restarts. Memory ended at 1.24 GiB. Ordinary sync and purgatory maintenance continued throughout. Together with the earlier 32-minute `15369ca` soak, this demonstrates bounded slot recovery without the reconnect train caused by the abandoned whole-session candidates. Reviewability: the runtime/test mechanism remains 1,491 additions/229 deletions across four files relative to master, plus a six-line changelog entry. In the largest file, `relay_connection.rs`, runtime ledger/lifecycle logic and focused invariant tests are deliberately colocated. The size reflects unifying every subscription consumer plus transactional live coverage and stale-session safety; the final timeout and admission corrections simplify the operational failure modes. A file-only extraction would move lines without reducing the correctness surface, so no cosmetic refactor is included. Part of `nostr:nevent1qqspxxvcaqj96zt8sdj520h9nl4rtq3873m6za6keg2gm2xn6dgp55cpz3mhxue69uhhyetvv9ujumn8d96zuer9wc85zw09`. |
||
|
|
6cccebb93b |
Merge #6b9488f1: fix(sync): adapt pagination to relay page size
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsxh9yg79mm2wkr7qu50s8hucq6wufr2aandvr2yvy3epg6zhxeleccjw758 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Production risk: historic REQ filters omit limit, while live Ditto-shaped relays can return only 100 events per filter and advertise only a much larger max_limit. The static threshold of 200 therefore treats a full 100-event page as exhausted and silently loses older history. Commit 1 counts every raw matching delivery before deduplication, rejected-event suppression, purgatory routing, or write policy, and derives the cursor from that same raw stream. Trace verification confirmed repeat deliveries reach ProcessResult::Duplicate. Commit 2 learns the largest raw page per relay connection session, combines it only with NIP-11 default_limit, and uses max(90, floor(0.9 * estimated_cap)). NIP-11 is fetched per session; max_limit never raises omitted-limit thresholds. A suspiciously short hinted page gets one cursor verification page, and any unseen event discards the hint for that session. Validation: cargo test --lib passed 623/623; three adaptive integration cases passed (Ditto-shaped 100/no-default/1000-max with 320 issues, honest default_limit, and lying-high default_limit); cargo check --workspace --all-targets passed. Known caveat: the required standalone req-concurrency test still fails on its documented pre-existing transient proxy limit defect and oldest-issue assertion. Under this diff the same too many concurrent REQs signature was noisier (five notices across two relay processes versus the documented baseline single rejection) because the lower learned threshold exposes more pagination pages when NIP-11 is unavailable. This PR does not mask or expand into that separate concurrency defect. |
||
|
|
e53d2f30ec |
Merge #2960a755: fix(sync): batch purgatory dependency polls by relay
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqszjc98249z0hz7zpf3gqmqyhxru0leqep5dvp77x8zur8z2msxmeg6adac7
PR-Author: DanConwayDev's Agent
nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0
CoverNote:
Production on gitnostr.com recorded four unresolved repositories producing 193 exact-ID relay fetch log entries and 482 requested event copies during the 2026-08-06 06:26:59-06:42:35 UTC window. The 30-second recovery cadence is intentional so authorization and state events are found promptly when they appear, but each purgatory repository currently spawns its own request to every hinted relay and competes with ordinary synchronization for per-connection query-rate budget.
This supersedes the closed standing-subscription proposal nostr:nevent1qqsfp9n5hq3q6x4hkr8pcv62ut5qjdvthlqgzpjlqmmx2cgd97h6zygpz3mhxue69uhhyetvv9ujumn8d96zuer9wc55cclu. That approach avoided repeated query creation but consumed scarce concurrent subscription slots and required substantially more lifecycle machinery.
Build one maintenance-round plan keyed by canonical connected relay, union all due cold dependency IDs from every selected purgatory announcement, and issue bounded 100-ID queries per relay. Preserve the existing 30-second cadence, parallel relay I/O, exact-ID retry accounting, authorization ordering, and early exit when dependencies resolve. Include repository_count and requested_count in the existing fetch log so production batching is directly observable.
The 100-ID chunk assumes an exact-ID filter can return at most one match per requested ID and stays within the lowest audited default result cap. This deliberately does not add live subscriptions, alter purgatory retention, add exponential backoff, or change general historic-sync requests.
Validated with cargo check --workspace --all-targets, all nine maintainer-reprocessing integration scenarios, and 47 sync unit tests. The new scenario indexes two cold invitations for separate repositories behind one relay and observes one recovery query with repository_count=2 and requested_count=2.
Production verification on exact PR commit
|
||
|
|
0a73d68da4 |
Merge #3509c127: fix(sync): suppress naughty relay connection attempts
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsr2zwpylk5cw0654n7hhrqgdd0jhc4pyyr8fmwf0lccwmks4f2fdqumxzdg PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Persistent DNS, TLS, and protocol failures were recorded in the relay naughty list, but reconnect eligibility and every scheduling path ignored that state. Production consequently kept announcing and launching retries after normal health backoff. Consult the existing tracker both when deciding reconnect eligibility and at the shared connection-scheduling boundary before lifecycle mutation or worker-capacity reservation. This preserves the required first dial, suppresses all later scheduling paths while the entry is live, and restores eligibility through the existing 12-hour expiry behavior. The scenario regression uses a deterministic broken WebSocket endpoint. It proves that the classification dial is the only connection accepted across multiple reconnect ticks and that no reconnect intent is emitted after the endpoint becomes naughty. This deliberately leaves error classification, expiry duration, Git-domain throttling, and transient relay backoff unchanged. ## Validation - Final PR commit: `ff388ce184430869c0e5e6fecadb4bf96aec951c`. - Full workspace suite passed: 617 library tests plus every integration and doc-test target. - Final sync target passed: 79 passed, 1 pre-existing ignored. - Focused health tests passed: 16 passed. - `git diff --check` passed. ## Production verification Deployed unmerged PR commit `ff388ce184430869c0e5e6fecadb4bf96aec951c` to gitnostr.com via nixos-fi1 commit `f030cfc987b128356ff856c45d45984f040c1ffc`. The exact flake pin, live ExecStart path `/nix/store/4clqvqy04lmz1q3fq6964r020x985kaf-ngit-grasp-2.0.0/bin/ngit-grasp`, and Nix registration time 2026-08-05 14:55:05 UTC independently identify the deployed revision. Activation began at 14:55:20 UTC. Observed production window: 2026-08-05 14:55:20–15:10:37 UTC (15m17s). Fourteen relays organically entered the naughty list. Every one had zero subsequent reconnect-intent lines, zero repeated event-directed target rejections, and zero repeated naughty-failure lines. During the same window, 110 legitimate reconnect attempts continued for non-naughty degraded relays, demonstrating that the reconnect scheduler remained active rather than globally stalled. No regression signal appeared during the soak: `NRestarts=0`; no error-priority journal entries, panics, scheduler failures, or stale-attempt results; memory peaked at approximately 1.26 GB. This production result, together with the deterministic scenario regression and green test suite, is sufficient for merge. Tracked under nostr:nevent1qqspxxvcaqj96zt8sdj520h9nl4rtq3873m6za6keg2gm2xn6dgp55cpz3mhxue69uhhyetvv9ujumn8d96zuer9wc85zw09 |
||
|
|
1c586d269c |
Merge #cc893bf3: feat(grasp-audit): emit full audit JSON reports
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsvezfm7w48p7z2sk05xs48pnjx7fddys0apezs9yvg7zrp9l7f9rsmathdx PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Expose the merged stable audit result model through audit --json using one schema-versioned run envelope. Completed audits include tool/run metadata, pinned specification revisions, aggregate counts, and per-test outcomes. Parse, setup, and runtime failures emit the same envelope with an error outcome. Human output and audit ordering remain intact. Exit status is 0 for pass, 1 for completed test failures, and 2 for invocation or runtime errors. |
||
|
|
9ca59b428a |
Merge #d10d26ba: fix(sync): retain NIP-77 for small hydration residuals
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsdzrfxh2zzfnhr0dmhdkf3y9gjsc53v0yj9vfwt4vl3xd9rqfyn0sg5j4k7 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Retain NIP-77 when an otherwise successful exact-ID hydration leaves a small residual. Semantic REQ+EOSE fallback now requires a material batch (at least 20 advertised IDs), an initial delivery rate of at most 10%, and a zero-progress residual retry. Other residuals keep NIP-77 enabled and enter bounded missing-event recovery. ## Why Production showed healthy or mostly healthy relays being marked incompatible solely because a small residual retry returned zero. `git.nostrhub.io` delivered 1,420/1,818 before withholding an expired residue; `nos.lol` delivered 115/118 and all six sampled residual IDs later became available by both NIP-77 and ordinary exact-ID REQ. Neither warrants semantic fallback. The threshold preserves the original compatibility path for the distinct case where roughly 90% of a material advertised inventory cannot be hydrated by ID but can be recovered semantically. ## Validation - Unit boundaries cover the four production ratios plus the 20-event/10% policy edges. - The censoring-proxy scenario exercises a partial first pass, zero-progress retry, degraded status, continued live sync, and bounded recovery; it proves the relay retains NIP-77 and does not enter semantic fallback. - Full `nix develop -c cargo test` green: 616 library tests; all integration targets; sync 78 passed / 1 pre-existing ignored; doc tests green. ## Production verification Deployed unmerged PR commit `a7da97e1054a5069fa67bd764389885de569fa7a` to gitnostr.com via nixos-fi1 commit `5845011`. The exact flake pin, systemd ExecStart path `/nix/store/jg5yi9pfq94xl52sgqk4das5cs5bnl8i-ngit-grasp-2.0.0/bin/ngit-grasp`, and Nix registration time 2026-08-05 12:59:38 UTC independently identify the deployed revision. Activation began at 13:01:19 UTC. Observed production window: 13:01:19–13:16:21 UTC (15m02s). Organic startup sync exercised the small-residual classification: `relay.ngit.dev` delivered 976/978 advertised events, and the deployed code retained NIP-77 and created one exact-ID retry for the two residual IDs. No material-hydration incompatibility was recorded and no semantic REQ+EOSE switch occurred. The relay had not returned the retry EOSE by the end of the window, so terminal zero-progress recovery is supported by the end-to-end scenario rather than claimed as organic production evidence. No regression signal during the window: `NRestarts=0`, 52 events synced, service remained active, and memory ended at approximately 1.26 GB, within the recent production envelope. The investigation also confirmed two separate facts outside this PR: all six sampled `nos.lol` residual IDs subsequently became available through both NIP-77 and ordinary REQ; and the purgatory recomputation path can target gitnostr.com itself, which remains a separate self-exclusion fix. This production exercise of the deployed residual classification, combined with the end-to-end terminal recovery scenario and full green suite, is sufficient for merge. |
||
|
|
a2215b9b8c |
Merge #b69eac8d: feat(grasp-audit): harden discovered-server probes
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqstd84v3473q4ts5jjtcmlh7az34pgxrvv49g89l9vyumjyxk4n3lqwpmgfv PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Add opt-in network hardening for scheduled read-only probes of untrusted GRASP endpoints. Validate public wss/https destinations, pin WebSocket and HTTP connections to pre-resolved addresses, refuse redirects and cross-origin clone URLs, and bound DNS resolution and response bodies. Hardening remains disabled by default so ws://localhost and other local development targets continue to work. Hardened probes cannot be combined with --create-repo. |
||
|
|
96190bef52 |
Merge #c9bce948: fix(sync): bound purgatory fetches for missing remote…
fix(sync): bound purgatory fetches for missing remote tips nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsvn08ffq3yerehv7cw56vs99sguq0hp039q7kjlx3wm2jhmncdtkq5chje3 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Fetch needed OIDs advertised as remote ref tips in one safe batch, isolate residual missing-object requests, and memoize confirmed per-URL misses while the sorted advertisement fingerprint remains unchanged. This prevents repeated purgatory passes from re-requesting hundreds of known-missing tips. ## Validation - Scenario regression fails on `master` with one failed upload-pack request carrying all ten wants. - On this branch, the first pass batch-fetches both advertised tips, isolates each unavailable tip to a single-want request, and leaves the unsatisfied state in purgatory. - A fresh replacement state event deterministically triggers a second pass; its unchanged advertisement adds zero failed upload-pack exchanges because the eight confirmed misses are memoized. - Helper tests cover stable order-independent advertisement fingerprints, invalidation on ref changes, 30-minute lazy expiry, and the 1,024-URL cap. - Full `nix develop -c cargo test` suite green: 615 library tests, all integration targets (sync: 78 passed / 1 ignored), and doc tests. ## Production verification Deployed unmerged PR commit `479d3b451f65519b181b6c4fd065873b718c5359` to gitnostr.com via nixos-fi1 commit `a491bc7`. The pin, systemd ExecStart store path `/nix/store/mqsxa5v38nfgmy5880g808xyrblyfq3g-ngit-grasp-2.0.0`, and its Nix registration time (2026-08-05 10:46:27 UTC) independently confirm the deployed revision; activation began at 10:47:40 UTC. Bounded soak: 2026-08-05 10:47:40–11:17:13 UTC (29m33s). The deployed fetch pipeline completed two organic advertised-tip passes in-window, and a third at 11:18:25: all three had `residual_attempted=0`, `residual_missing=0`, and fetched every needed advertised tip. Counters at 11:18 showed 3 passes / 3 advertised tips / 3 fetched. The production workload did not enter the remote-missing branch; that branch is covered by the end-to-end repeated-pass scenario above. No regression signal during the soak: `NRestarts=0`, watchdog reclaims 0, 477 events synced, 105 relays tracked / 66 connected at the end of the window, and memory 1.34 GB (within the prior ~1.4 GB baseline). Serving-side `not our ref` errors continued from an external third-party crawler as expected and are not generated by gitnostr.com's outbound fetch pipeline. This split of evidence—real production exercise of the exact deployed pipeline plus end-to-end scenario exercise of the memoized repeated-miss path—is sufficient for merge without manufacturing public production events. |
||
|
|
af4e40db36 |
Merge #d46c9733: feat(grasp-audit): define stable audit results
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsdgmyhx0qyy0423kure57ppcu6rz4jhkefrsnztphn037pypa2wfqk8gzvp PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Add serializable per-spec and per-test report data without wiring a new CLI output mode. Pin suite metadata to the audited GRASP revisions, expose explicit outcomes and millisecond durations, and bound diagnostic text safely. Use typed probe check names and shared GRASP-06 test identifiers so emitted IDs come from one source while preserving existing human and probe JSON output. |
||
|
|
c0e7af7055 |
Merge #cd4d6f74: feat(grasp-audit): support explicit audit identities
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsv6nt0ws0mrn9utmr9atvgvcpp0qq4nk66mqujwlsvz83nd7trl4cmtj3nc PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Use the same identity inputs for probes and full audits: inline --nsec, --nsec-file, or GRASP_AUDIT_NSEC. Parse nsec and hex keys, retain generated-key fallback, and share the environment name with Git subprocess secret scrubbing. |
||
|
|
958aef2af4 |
Merge #7d13802c: fix(grasp-audit): keep signing secrets out of git
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs86yuq93gmfaa9323eymlnpjt7lqlc50nypkkms4yf30cgk5u6ljc7p9luw PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Strip both the audit identity and relay-owner secret variables at the existing hermetic git subprocess boundary. This prevents git hooks, credential helpers, remote helpers, and their descendants from inheriting signing keys. |
||
|
|
320f3fdae1 |
Merge #a8536225: fix(sync): bound concurrent negentropy rounds per rela…
fix(sync): bound concurrent negentropy rounds per relay connection
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs2s5mzy4kddgl27xq9s02cf5evatgqkm63x9xqyeapfdrahu9wtfcdudjkr
PR-Author: DanConwayDev's Agent
nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0
CoverNote:
Startup historic sync opened one NIP-77 negentropy diff per filter with no bound: an unbounded join_all launched every diff in a batch simultaneously, so a large watched set (146 filters on the bootstrap relay) burst past relay per-connection subscription budgets. On strfry-family relays negentropy views share maxSubsPerConnection with ordinary subscriptions; nos.lol (budget 20) answered a gitnostr.com startup with 34 "too many concurrent NEG requests" rejections, 61 per-filter timeouts, and 21 failed fallback-subscription creations in two minutes (2026-08-04, observed during the cycle-2 soak of
|
||
|
|
633224bc81 |
Merge #c1b384df: fix(sync): use bounded cooldown for transient negentro…
fix(sync): use bounded cooldown for transient negentropy failures
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsvrvuyml2nqtsnpfjz0vrmrfnw52apdvggskpwpr82pqkt6aaq0dqy9k7hy
PR-Author: DanConwayDev's Agent
nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0
CoverNote:
One transient negentropy diff error permanently disabled NIP-77 for the relay connection. On gitnostr.com (2026-08-04, PR commit
|
||
|
|
be71c2a580 |
Merge #e2dfa0e5: fix(sync): stop refetching structurally malformed even…
fix(sync): stop refetching structurally malformed events every historic pass
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsw9haquk7w32zavgeyqevzxn2uwkmj8z0ufe8wsadn8tt0mxq245grk39n0
PR-Author: DanConwayDev's Agent
nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0
CoverNote:
Production evidence (2026-08-04, deployed
|
||
|
|
f90107c679 |
Merge #02142b44: fix(sync): preserve proactive sync under relay REQ lim…
fix(sync): preserve proactive sync under relay REQ limits nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsqy9ptg3h6de3wypkmfyed2pgy5gamaxzsfjacrh7w8ewvlkzfq5cefuqpy PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Public relays cap active subscription IDs, while GRASP-02 previously opened separate live and historic REQs for each state, repository-tag, and root-event filter. Rejection of the tail removed live coverage and could leave historic work incomplete. This three-commit series first adds multiple-filter NIP-01 REQs at the transport layer, then batches up to ten compatible proactive filters under each subscription ID while preserving independent event counts, oldest timestamps, and pagination frontiers. The existing 300-ID negentropy fetch chunks deliberately remain one filter per REQ so their message-size bound is preserved. Grouped historic pagination explicitly assumes that relay result limits apply independently per filter, the effective per-filter cap is at least 75, and no additional total-result cap can starve filters within a grouped REQ. These assumptions are recorded in the implementation and design documentation. Rate-limit recovery now keeps the first deadline during an active cooldown, starts a fresh cooldown when a post-deadline recovery attempt is rejected, and does not confuse WebSocket connection success with REQ acceptance. The integration regression uses a relay capped at four active REQs and synchronizes on an observed q-tagged event rather than a fixed sleep. Formatting, warnings-as-errors Clippy, and the complete ngit-grasp test suite pass. General asynchronous NIP-01 CLOSED recovery is inherited behavior and is intentionally left for focused follow-up work. |
||
|
|
e889ffa4a7 |
Merge #c63bcd0b: fix: prune stale purgatory states after promotion
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsvvw7dpwceddf093cgt8rhdgrudfsmw25u3296cqw526dggel234cr8039y PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: ## Summary After a repository state is successfully promoted from purgatory, remove only non-preferred states on the same author and identifier when the locally available Git objects cannot reconstruct them. ## Preserved behavior - State promotion remains oldest-first so reconstructable predecessors enter relay history before replacements. - Equal timestamps retain the lower event ID under NIP-01. - Reconstructable predecessors and other maintainer coordinates remain available as rollback candidates. - Lifecycle-rejected saves do not trigger cleanup. ## Deliberately out of scope This revision does not change receive-pack locking, push authorization, deletion handling, persistent watermarks, queuing, or admission control. Transactional receive-pack and DoS-resistant admission are a separate follow-up project. ## Validation - 570 library tests - 42 purgatory-sync tests - workspace clippy with warnings denied - two rounds of independent review by three reviewers Latest narrow revision: nostr:nevent1qqs2zv3rdvpcnsgnjtr5tm29r6vspgx9vv2jhur87u4uqrqaqutgkmspz3mhxue69uhhyetvv9ujumn8d96zuer9wcpzqetunatx5f3844mpje54xc0pmq2tmdy7s7us7ndcrraer3vac9p2jvp827 |
||
|
|
49497629f7 |
Merge #28ce7595: Keep relay owner secrets out of process arguments
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsz3nn4jh38w8aa5ty7fg5cuxncmers3gwgupw90nmw7nfvmytpl2cne0c5v PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Fixes NixOS deployments exposing the relay-owner nsec through a shell-expanded ExecStart command. - removes the relay-owner secret from CLI parsing and loads it from the relay_owner_nsec systemd credential, NGIT_RELAY_OWNER_NSEC/.env, or the persistent fallback in that order - rejects empty or invalid provisioned identities instead of silently generating a replacement, and creates fallback key files with private permissions - changes the NixOS module to LoadCredential plus a direct binary ExecStart and documents the operator migration Regression coverage verifies argv rejection, credential trimming and validation, credential precedence, environment handling, fallback behavior, and generated file permissions. The full 566-test library suite and binary target pass. Compatibility: direct --relay-owner-nsec users must move the value to NGIT_RELAY_OWNER_NSEC or a credential/file. The existing NixOS relayOwnerNsecFile option remains supported, but its source file must exist when systemd starts the service. This PR does not deploy or rotate any production key. |
||
|
|
438080c191 |
Merge #dd459383: docs(nix): avoid forcing unfiltered local module sourc…
docs(nix): avoid forcing unfiltered local module sources nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsd63vns0w7hayc350m0uymv9ummy058j2kff8m706sfgvecwh6kegdckpha PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 CoverNote: Documents resource-safe NixOS module validation for ngit-grasp. Importing `nix/module.nix` from a working tree is lazy by itself. The expensive path begins when rendering an enabled service forces the module-built package through `ExecStart`; `buildRustPackage` then coerces `src = ../.` relative to that local module and can recursively hash or copy an unfiltered checkout. Git-backed flake imports avoid this because `../.` resolves inside the already filtered store source. The guide now recommends a Git-backed module or non-forcing builder stub, evaluation without builds, dry-run derivation inspection, one shared package derivation across instances, and conservative initial build limits. It also records the separate risk of accidentally forcing two ngit-grasp packages with different Rust toolchains concurrently. |
||
|
|
be18f6083c |
Merge #d6e07abe: Fix invitation recovery during busy relay startup
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqsddcr6h6cafdqp6qmcxh9cd8fxc6tr7nc6asx60krjxeuqx34zkjc9p0928 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: Fixes maintainer invitation acceptance that remains in purgatory while a production GRASP server starts and ingests a large bootstrap history. The live reproduction showed two independent blockers: startup waited minutes for retention cleanup across more than 52,000 deletion requests, then a rate-limit notice and paginated EOSE formed a circular wait on the SyncManager lock. Fresh invitation announcements were accepted into purgatory, but the five-second recovery pass could not run. This PR starts retention catch-up after the relay and sync workers are live, resumes the exact historic page outside the actor lock after rate-limit cooldown, and replaces repeated full-index action construction with lightweight dirty-relay recomputation. Coverage verifies deferred retention cleanup, preserves generic pagination while the actor remains available, limits relay recomputation to the changed batch, and keeps the existing maintainer invitation/state integration scenarios passing. |
||
|
|
e6a0451200 |
Merge #565b5f5c: Fix receive-pack completion readiness and timeouts
nostr:nevent1qgsx2lyl2e4zvfadwcvkd9fkrcwczj7mf858hy85mwqclwgut8wpg2spz3mhxue69uhhyetvv9ujumn8d96zuer9wcq3yamnwvaz7tm8d96xummnw3ezucm0d5q3kamnwvaz7tmwva5hgtnyv9hxxmmwwashjer9wchxxmmdqqs9vk6ltnmqd7tmte7pt53svndcz8w7eh68mg5kt2tckjyv3fdkllq2hx629 PR-Author: DanConwayDev's Agent nostr:npub1v47f74n2ycn66asev62nv8sas99akj0g0wg0fkup37u3ckwuzs4q7cwtp0 PR description: ## Context Commit |