Commit Graph
452 Commits
Author SHA1 Message Date
jcoffey-dev 8d3e99bc00 Legal holds, step 4: freezing, release, and the audit log
Placing or widening a hold freezes what's already archived in its scope
and range, its old deadline noted; releasing one gives each item no other
hold covers that deadline back, or release plus 30 days if later. One
pass over the archive does both and changes nothing twice (LH-6, LH-10,
LH-11). A held archived item can't be destroyed; restoring still can,
and the hold is named only to callers who may see holds (LH-7). Audit
records about a held account survive the purge (AU-7).

Fixes the daily clean-up of expired archived items (UD-13), which never
found any: the registry's unfiltered query reads an all-ids index that
archived items aren't in. Items are now walked account by account, kept
deleted accounts included. Expired items were still removed whenever
their account's archive was read.
2026-09-27 19:33:07 -07:00
jcoffey-dev 7b97efbb7f Legal holds, step 3: deleted items in a held account are kept
Every way of deleting mail (JMAP, IMAP EXPUNGE, POP3, mailbox removal,
Trash emptying) and Sieve scripts, events, contacts and files now asks
how the account's deletions are kept: a hold keeps them with no expiry
(archivedUntil 9999-12-31), even with undelete off; otherwise undelete's
period applies as before (LH-4).

A hold's date range decides by the item's own date (LH-3). Mail is noted
as held at deletion and settled when it's archived, once its received
date is known; outside the range it gets undelete's deadline or isn't
kept. Events go by their start, with a day's slack for time zones;
recurring events, contacts, files and scripts are held whole.

A groupware item's note now stays until its archive succeeds, and a
failure retries the task instead of being logged and lost (LH-5).
2026-09-27 19:33:07 -07:00
jcoffey-dev 318783f444 Legal holds, step 2: who a hold covers
A hold reaches an account by name, through any of its addresses'
domains, its groups or its tenant, as they are now, so an account added
to a held domain later is held too. An account that leaves a held
domain, group or tenant stays held: the registry write hook adds it to
the hold by name on every account change, whoever makes it (LH-2).
Server::holds_on answers for the deletion paths, from the store each
time so a hold binds every node at once.
2026-09-27 19:33:06 -07:00
jcoffey-dev 5d2e35b2dc Legal holds, step 1: the hold itself
inbuxa:LegalHold get/set places a hold on accounts, groups, domains,
tenants or the whole server, with an optional date range. A hold's range
and scope can only widen, a released hold is read-only, and none is ever
deleted. Placing, changing and releasing each need a reason and are
audited (LH-1, LH-3, LH-10, AU-12).

Permissions 669-672 (see, place, widen or release, export held data)
go to server administrators only; the tenant ceiling always strips them,
as it does Impersonate (LH-13). Schema: Compliance > Legal Holds.

What a hold keeps comes next, through the undelete hooks.

Also moves the lock expiry helpers below the lock module's imports.
2026-09-27 19:33:06 -07:00
jcoffey-dev 621ebdff74 Merge pull request 'Release 2026.9.27.2' (#69) from release-2026.9.27.2 into main
publish / publish-arm64 (push) Successful in 39m17s
publish / binaries (push) Successful in 33s
publish / announce (push) Successful in 23s
ci / fork-checks (push) Successful in 14s
publish / version (push) Successful in 32s
publish / publish-amd64 (push) Successful in 25m41s
publish / release (push) Successful in 1s
ci / build (push) Successful in 37m4s
v2026.9.27.2
2026-09-28 01:35:26 +00:00
jcoffey-dev 355bd3a40e Release 2026.9.27.2
ci / fork-checks (pull_request) Successful in 1m23s
ci / build (pull_request) Successful in 7m16s
Delegates reach the whole locked account (#68): its calendars, contacts
and files as well as its mail, even a kind it holds none of yet, and
writing delegates may add at the top of its Files.

Prepared Explain answers relabeled for this release; no setting changed
since 2026.9.27.1, so all 706 carry over.
2026-09-27 18:27:33 -07:00
jcoffey-dev f7a63b9ed0 Merge pull request 'Delegates reach the whole locked account' (#68) from fix/delegate-whole-account into main
ci / fork-checks (push) Successful in 48s
ci / build (push) Canceled after 12m39s
2026-09-28 01:22:48 +00:00
jcoffey-dev 9f6761c9dd Writing delegates may add at the top of a locked account's Files
ci / fork-checks (pull_request) Successful in 17s
ci / build (pull_request) Successful in 17m56s
A shared account refuses top-level folders, so an organize or full
delegate couldn't add anything to a locked account with no folders. A
delegate who may write now can, as the owner could; the reconcile after
the create grants it the new folder. Read delegates still can't (AL-6,
AL-7).
2026-09-27 18:04:24 -07:00
jcoffey-dev d4d127fa7d Delegates reach the whole locked account
ci / build (pull_request) Canceled after 5m27s
ci / fork-checks (pull_request) Successful in 14s
A delegate's token listed the locked account only for kinds of data it
held grants on, so one with no files (or no calendar) was refused to the
delegate outright: "You do not have access to account". The token now
lists the locked account for mail, calendars, contacts and files alike,
so an empty kind reads as empty. What the delegate may see or change is
still each container's grant (AL-7).
2026-09-27 17:58:50 -07:00
jcoffey-dev 7720a57ac9 Merge pull request 'Release 2026.9.27.1' (#67) from release-2026.9.27.1 into main
publish / publish-amd64 (push) Successful in 28m13s
ci / build (push) Successful in 34m51s
publish / release (push) Successful in 2s
publish / publish-arm64 (push) Successful in 42m7s
publish / binaries (push) Successful in 39s
publish / announce (push) Successful in 22s
publish / version (push) Successful in 28s
ci / fork-checks (push) Successful in 14s
v2026.9.27.1
2026-09-27 23:43:31 +00:00
jcoffey-dev 30ea43d019 Release 2026.9.27.1
ci / fork-checks (pull_request) Successful in 13s
ci / build (pull_request) Successful in 7m33s
The audit log (#64): every administrator change, admin sign-in and look
into someone else's data, recorded before it happens, chained per node
and checkable for tampering, exportable with a manifest, kept 2 years.

Locked accounts (#65, #66): an account that keeps receiving mail but
can't sign in and sends nothing on its own, handed to delegates at read,
organize or full, ending at a date when one is set.

Prepared Explain answers relabeled for this release; no setting changed
since 2026.9.27, so all 706 carry over.
2026-09-27 16:35:42 -07:00
jcoffey-dev dd3eec3936 Merge pull request 'End a locked account's delegation at its date' (#66) from fix/delegation-until into main
ci / fork-checks (push) Successful in 1m1s
ci / build (push) Canceled after 12m1s
2026-09-27 23:31:30 +00:00
jcoffey-dev a36236efff End a locked account's delegation at its date
ci / fork-checks (pull_request) Successful in 44s
ci / build (pull_request) Successful in 4m59s
A delegation with an end date dropped out of the delegate's token then,
but its folder grants stayed until the daily sweep, so the delegate kept
the account as an ordinary share for up to a day. Each node now sleeps
until the soonest end date, woken early by any lock write and at least
hourly, and re-applies that lock under a cluster-wide claim.

The sweep also had a second-run bug: a delegation past its date gave the
delegate back its earlier share, then dropped the note, so the next sweep
removed that share entirely. The note is now kept while the delegate is
still listed.
2026-09-27 16:26:04 -07:00
jcoffey-dev 224597cab2 Merge pull request 'Locked accounts: keep receiving mail, no sign-in, hand to delegates' (#65) from feature/account-lock into main
ci / fork-checks (push) Successful in 14s
ci / build (push) Canceled after 35m53s
2026-09-27 22:55:36 +00:00
jcoffey-dev 447229f871 Lock accounts: keep receiving mail, no sign-in, hand to delegates
ci / fork-checks (pull_request) Successful in 1m4s
ci / build (pull_request) Successful in 8m47s
A locked account can't sign in (it fails as a wrong password does), its
sessions end on every node, refresh tokens stop working, and its Sieve
scripts forward and reply to nothing. Mail keeps arriving.

Delegates get real ACL grants on the account's mailboxes, calendars,
address books and files at read, organize or full, with the rights they
replaced restored on unlock. Folders made later are granted after the
create and in a daily sweep. Organize delegates can't destroy; send-as
needs organize or full. The JMAP session marks delegated accounts in
urn:inbuxa:jmap.

New inbuxa:AccountLock object with get/set, permissions 665-668, and a
Compliance > Locked Accounts entry in the schema. Lock, unlock and
delegate changes need a reason and are audited; delegate access and
writes are audited too (audit-hold-lock spec AL-1 to AL-12).
2026-09-27 14:46:06 -07:00
jcoffey-dev ebf2fe11d9 Merge pull request 'Audit log: a permanent, tamper-evident record of admin actions' (#64) from feature/audit-log into main
ci / fork-checks (push) Successful in 1m22s
ci / build (push) Successful in 21m54s
2026-09-27 21:45:50 +00:00
jcoffey-dev 86d7ebd982 Audit log: a permanent, tamper-evident record of admin actions
ci / fork-checks (pull_request) Successful in 52s
ci / build (pull_request) Successful in 1h4m15s
What administrators and the server itself do to the control plane is now
recorded, from inbuxa-drafts/specs/audit-hold-lock.md (AU-1 to AU-12):
settings, accounts, domains, roles and every other registry change, with
each field's before and after (secrets only as "changed"); the fork's own
settings objects; administrator sign-ins (and failed ones to administrator
accounts), master-user and recovery-admin sign-ins, once an hour per
account, method and address; access to another account's data through
impersonation or FetchAnyBlob, once an hour; exports and tamper checks;
and registry writes the server makes on its own, named by subsystem
(system:AcmeRenewal, system:auto-ban, system:directory-sync, ...), with a
spam rules update as one summary record.

No change without its record (AU-3): before a set method changes anything,
a pending record per requested create, update and destroy is written; if
that fails, the method is refused with serverFail. Its outcome follows as
a later entry. A change interrupted by a crash stays "unfinished".

Records live in the fork's subspace under L, as one SHA-256 hash chain per
node. The chain's head is stored, never cached, and every append asserts
it, so two writers can't take the same place. Nothing can edit or delete
a record; the daily purge removes the oldest past the retention (default
730 days, minimum 90) and records where the chain now starts, so
verification still passes. security.audit-recorded (647) copies each
record to webhooks, OpenTelemetry and the log; security.audit-write-failed
(648) reports a failed write.

New JMAP objects under urn:inbuxa:jmap: inbuxa:AuditEvent/get and /query
(filters: time, actor, action, target, account, tenant, outcome, address,
text), inbuxa:AuditSettings, inbuxa:AuditExport (CSV or JSON Lines built
on the server, each line with its chain hash, ending in a manifest; the
created object names the blob and its SHA-256) and
inbuxa:AuditVerification. New permissions sysAuditGet, sysAuditExport and
sysAuditSettingsUpdate: the Administrator role gets all three, the Tenant
Administrator role gets read and export, once, on existing installs too.
A tenant administrator sees records whose actor or target is in its
tenant, including a server administrator's changes there.

Sign-in method on the session: access tokens now remember how they signed
in (password, app password, API key, OAuth client, directory, master user,
recovery admin), including across the HTTP credential cache. New OAuth
access tokens carry their client id in the sealed claims; older ones show
as client "unknown" until they expire.

The schema gains the permissions, the two events and a Management >
Compliance > Audit Log link.

Stack: the request layer boxes every inner future where it's made. Without
that, a debug build overflowed the default 2 MB worker stack on a registry
set; measured with the same request, the branch and main now overflow at
the same stack size (between 1856 and 1920 KiB, debug), so the layer adds
nothing measurable.

Tests: unit tests in inbuxa-features and jmap; system::audit::audit_log_tests
(run with --ignored) passes on RocksDB, SQLite, PostgreSQL, PostgreSQL with a
read replica, MySQL, MySQL with a replica and FoundationDB. The system, JMAP
and SCIM suites pass. authorization.rs skipped fork permissions that guard
no registry object; the audit suite checks a plain user is refused instead.
2026-09-27 13:40:12 -07:00
jcoffey-dev d3ebfb79f9 Merge pull request 'Release 2026.9.27' (#63) from release-2026.9.27 into main
ci / fork-checks (push) Successful in 28s
publish / version (push) Successful in 29s
publish / publish-amd64 (push) Successful in 24m11s
publish / release (push) Successful in 1s
ci / build (push) Successful in 35m44s
publish / publish-arm64 (push) Successful in 35m26s
publish / binaries (push) Successful in 33s
publish / announce (push) Successful in 22s
v2026.9.27
2026-09-27 05:18:54 +00:00
jcoffey-dev 833e6871f7 Release 2026.9.27
ci / fork-checks (pull_request) Successful in 45s
ci / build (pull_request) Successful in 4m51s
inbuxa's own mark (#62): the kitten over a server with a bay for each
piece of the suite, on the built-in sign-in and RSVP pages, the web
logo and the email logo.

Prepared Explain answers relabeled for this release; no setting changed
since 2026.9.26.1, so all 706 carry over.
2026-09-26 22:13:54 -07:00
jcoffey-dev 056bbb179d Merge pull request 'Brand: inbuxa own kitten replaces ihasmail cat' (#62) from brand/new-mark into main
ci / fork-checks (push) Successful in 53s
ci / build (push) Canceled after 6m7s
2026-09-27 05:12:44 +00:00
jcoffey-dev d7182f4511 Brand: inbuxa's own kitten replaces ihasmail's cat
ci / fork-checks (pull_request) Successful in 43s
ci / build (pull_request) Successful in 3m18s
inbuxa's mark was ihasmail's cat-and-envelope reused unchanged. The new
one keeps the family's face, paws and colors, over a server with a bay
for each piece of the suite: the letter (webmail), a prompt (console),
status lights (server).

- The built-in sign-in and calendar RSVP pages, and the web logo, drew
  the old cat as an embedded PNG. They now draw the mark as vector in
  the same slot, keeping class="symbol"; each page is about 31 KB
  lighter. The .min copies are updated the same way and the .min.gz
  regenerated with gzip -9 -n, as minify_html.sh does.
- resources/branding: email-logo.png (the compact lockup at 380x80 on
  white, as before) with its .b64 regenerated byte-for-byte in the old
  76-column form, and favicon-64.png.
- img/brand: the logo bundle, now pure vector, with its README.
2026-09-26 22:05:17 -07:00
jcoffey-dev 055752f3a3 Merge pull request 'Announce releases on the community forum' (#61) from announce-releases into main
ci / fork-checks (push) Successful in 1m19s
ci / build (push) Successful in 1h28m25s
2026-09-27 02:40:21 +00:00
jcoffey-dev 07557ba8e2 Announce releases on the community forum
ci / fork-checks (pull_request) Successful in 45s
ci / build (pull_request) Successful in 6m11s
announce.yml runs coffey-labs/actions discourse-release on every published
release, posting it to this project's Announcements category on
community.coffeylabs.org. The release workflow also announces
from its own job, since a release made with the job token fires no
'on: release' workflow in Gitea.
2026-09-26 19:28:04 -07:00
jcoffey-dev f896e0cf3c Merge pull request 'Release 2026.9.26.1' (#60) from bump/2026.9.26.1 into main
ci / fork-checks (push) Successful in 26s
publish / version (push) Successful in 32s
publish / publish-amd64 (push) Successful in 28m22s
publish / release (push) Successful in 7s
ci / build (push) Successful in 38m11s
publish / publish-arm64 (push) Successful in 43m49s
publish / binaries (push) Successful in 1m3s
v2026.9.26.1
2026-09-26 23:43:31 +00:00
jcoffey-dev bed0d72e3f Release 2026.9.26.1
ci / fork-checks (pull_request) Successful in 51s
ci / build (pull_request) Successful in 11m54s
Prepared Explain answers relabeled for this release; 11 settings whose
default is the time of creation drop out, since their answers could never
match.
2026-09-26 16:31:09 -07:00
jcoffey-dev fdbc72e574 Merge pull request 'Explain: shorter answers, streamed, remembered, and prepared for settings' (#59) from feature/explain-faster into main
ci / fork-checks (push) Successful in 19s
ci / build (push) Canceled after 13m22s
2026-09-26 23:30:07 +00:00
jcoffey-dev ad648d8d12 Calibration test: pass the new stream argument to request::body
ci / fork-checks (pull_request) Successful in 50s
ci / build (pull_request) Successful in 4m29s
2026-09-26 16:25:30 -07:00
jcoffey-dev 7e7eca0883 Explain: shorter answers, streamed, remembered, and prepared for settings
ci / fork-checks (pull_request) Successful in 1m31s
ci / build (pull_request) Failing after 5m18s
ai-explain spec, amendment 1 (EX-22 to EX-28):
- answers are three or four sentences, max_tokens 160, cut at 700 chars;
- POST /api/explain streams the answer as server-sent events;
- each node remembers answers in memory (1,000, 24 h), keyed by the facts,
  prompt version and model, shared by server-level administrators;
- resources/explain/settings.json.gz ships answers for settings at their
  defaults, generated with prepare_setting_explanations (717 for 2026.9.27);
- the system prompt no longer carries the per-request marker, so a model
  server can reuse it;
- inbuxa:Explanation gains source, answeredAt and preparedFor.
2026-09-26 16:11:43 -07:00
jcoffey-dev e50222d518 Merge pull request 'Release 2026.9.26' (#58) from bump/2026.9.26 into main
ci / fork-checks (push) Successful in 23s
publish / version (push) Successful in 28s
publish / publish-amd64 (push) Successful in 23m47s
publish / release (push) Successful in 1s
ci / build (push) Successful in 36m44s
publish / publish-arm64 (push) Successful in 35m54s
publish / binaries (push) Successful in 33s
v2026.9.26
2026-09-26 21:33:50 +00:00
jcoffey-dev 499c290e51 Release 2026.9.26
ci / fork-checks (pull_request) Successful in 47s
ci / build (pull_request) Successful in 12m18s
2026-09-26 14:21:12 -07:00
jcoffey-dev 0fdd11aa27 Merge pull request 'Don't listen on a socket whose bind failed' (#57) from fix/unbound-listener into main
ci / fork-checks (push) Successful in 47s
ci / build (push) Successful in 21m4s
2026-09-26 08:48:03 +00:00
jcoffey-dev b6f943a77c Don't listen on a socket whose bind failed
ci / fork-checks (pull_request) Successful in 46s
ci / build (pull_request) Successful in 10m37s
When a listener couldn't bind its address (a port below 1024 without
root, a port already in use, or the legacy-protocols switch putting a
listener back after privileges were dropped), the bind error was
reported but the socket was still passed to listen(). The kernel then
bound it itself, to a random port on every interface, and the server
logged the listener as started on the port it was configured with.

listen() now refuses a socket that isn't bound, so the listener is
reported with a listen error and skipped, and nothing opens anywhere
unexpected.
2026-09-26 01:36:39 -07:00
jcoffey-dev b41dfa7a1d Merge pull request 'Explain this: the local model reads delivery failures, verdicts, logs and settings' (#56) from feat/ai-explain into main
ci / fork-checks (push) Successful in 18s
ci / build (push) Canceled after 17m18s
2026-09-26 08:30:45 +00:00
jcoffey-dev 866d7d3ed5 Explain a setting that was never saved, from its defaults
ci / fork-checks (pull_request) Successful in 20s
ci / build (pull_request) Successful in 7m26s
A singleton such as x:SpamSettings has no stored object until someone
saves it; /get shows its defaults instead. Explain looked only for the
stored object, so every setting still at its defaults answered "No
such x:SpamSettings." It now falls back to the defaults the same way.
2026-09-26 01:09:46 -07:00
jcoffey-dev d9a6db025b Explain this: the local model reads delivery failures, verdicts, logs and settings
ci / fork-checks (pull_request) Successful in 48s
ci / build (pull_request) Successful in 9m4s
A new method, inbuxa:Explanation/set, asks the node's local model for a
short plain-words reading of one thing an administrator is looking at:
a failed recipient in the queue, a Classify verdict, a log line or trace
event, or one setting with its saved value. The server builds the prompt
itself from stored data and the registry schema, never from text the
console sends, and grounds SMTP replies in RFC 3463 and RFC 5321.

What the model is never shown: secrets (including ones nested inside a
setting, like an AI model's HTTP auth), raw protocol events, and the
contents of any other event. A tag name that doesn't have a tag's shape is
refused before a model is asked.

Calls share the AI gate with spam classification, but mail always keeps
its slot, and Explain has its own hourly count per account and its own
on/off switch in inbuxa:AiLimits. The permission is sysAiExplain,
superuser only; tenant administrators can't use it. The session carries
an aiExplain flag so a console knows when to offer the button.

An install whose roles were stored before the permission existed gets it
added once, at start-up, to the roles that are administrators' alone,
not the User role their defaults share with every account. An operator
who removes it later isn't overruled.

Tests: unit tests in inbuxa-features and jmap, and ai_explain_tests
(run with --ignored) covering the acceptance tests and the upgrade.
2026-09-26 00:52:51 -07:00
jcoffey-dev 96b54ede4e Merge pull request 'Release 2026.9.25.1' (#55) from bump/2026.9.25.1 into main
ci / fork-checks (push) Successful in 18s
publish / version (push) Successful in 33s
publish / publish-amd64 (push) Successful in 23m45s
publish / release (push) Successful in 1s
ci / build (push) Successful in 37m8s
publish / publish-arm64 (push) Successful in 35m1s
publish / binaries (push) Successful in 39s
v2026.9.25.1
2026-09-25 08:23:55 +00:00
jcoffey-dev 1f9b3174de Release 2026.9.25.1
ci / fork-checks (pull_request) Successful in 17s
ci / build (pull_request) Successful in 7m17s
2026-09-25 01:15:55 -07:00
jcoffey-dev 5e2ddf644f Merge pull request 'Report a node unhealthy after three minutes of silence' (#54) from feature/node-heartbeat into main
ci / fork-checks (push) Successful in 45s
ci / build (push) Canceled after 8m9s
2026-09-25 08:15:44 +00:00
jcoffey-dev 5245abd08d Report a node unhealthy after three minutes of silence
ci / fork-checks (pull_request) Successful in 38s
ci / build (pull_request) Successful in 7m24s
Every node renews its lease once a minute instead of every 30 minutes,
so the lease works as a heartbeat. x:ClusterNode reports a node Stale
once it has gone three minutes without renewing (it used to take an
hour), and Inactive after a day, as before.

Taking over a lease still needs a full hour of silence. A node that is
slow rather than gone never loses its id to another host, so snowflake
ids stay unique.

The admin dashboard's Cluster Health card counts these statuses.
2026-09-25 01:05:15 -07:00
jcoffey-dev 9fa5433665 Merge pull request 'Release 2026.9.25' (#53) from bump/2026.9.25 into main
ci / fork-checks (push) Successful in 1m9s
publish / version (push) Successful in 1m1s
publish / publish-amd64 (push) Successful in 28m2s
publish / release (push) Successful in 1s
ci / build (push) Successful in 39m42s
publish / publish-arm64 (push) Successful in 40m42s
publish / binaries (push) Successful in 1m6s
v2026.9.25
2026-09-25 05:35:39 +00:00
jcoffey-dev f2605877f7 Release 2026.9.25
ci / fork-checks (pull_request) Successful in 21s
ci / build (pull_request) Successful in 7m36s
2026-09-24 22:27:11 -07:00
jcoffey-dev c521f060ba Merge pull request 'PostgreSQL search: find words inside URLs and file names in body text' (#52) from fix/pg-url-body-tokens into main
ci / fork-checks (push) Successful in 47s
ci / build (push) Canceled after 53m30s
2026-09-25 04:42:05 +00:00
jcoffey-dev 5927dda7e2 PostgreSQL search: find words inside URLs and file names in body text
ci / fork-checks (pull_request) Successful in 48s
ci / build (pull_request) Successful in 3m26s
After #37, address fields on PostgreSQL are split into words as the
built-in index splits them, but language text (subject, body,
attachments) still goes straight to PostgreSQL's parser, which keeps a
URL, host, path or file name as tokens of its own:
"https://x.example/shipping-support/" becomes a url, a host and a
url_path, "invoice-2024.pdf" a file. So TEXT/BODY "shipping" missed
messages where the word appears only inside a link, while RocksDB and
the other built-in backends found them: 8 messages across a handful
of searches in the rehearsal.

On insert, language text is now indexed as it was, followed by the
word parts of each token that holds a URL separator (/ . @ : ? = & # _
% + ~ \), split with SpaceTokenizer as keyword_terms() splits addresses.
The parts go through the same text search configuration as the rest of
the text, so they are stemmed like the words around them. Plain words,
words that only carry punctuation ("end.", "(see") and hyphenated words
(the parser already splits those) add nothing, so text without links
is indexed exactly as before. Each part is added once per document.
On sample mail, the text vector of a short order notice with three
links grows from 546 to 716 bytes, a newsletter with 25 tracking links
from 5586 to 6430, and a plain letter not at all.

On search, a query word written as a URL, host, file or hyphenated word
also matches as its word parts, ORed with the query as written, so
"shipping-support" or "invoice-2024.pdf" match the new parts and
documents indexed before this change still match as they did.

Existing messages keep their old vectors until they are reindexed (the
reindexAccounts task); new and reindexed messages match at once.

store::search_tests gains test_url_word_search: five bodies, 19 body
searches for words found only in a URL path, query string, host or
file name, the tokens as written, plain words and non-matches, with the
same expected ids on every backend. It passes on RocksDB, SQLite,
MySQL and PostgreSQL; on main PostgreSQL fails at the first ("shipping"
finds [3], not [0, 3]). On PostgreSQL the suite then stops at the
account sort assertion (query.rs:689) exactly as it does on main.
2026-09-24 21:35:12 -07:00
jcoffey-dev 9e49597ae4 Merge pull request 'Settings writes: wait for a burst to settle before reloading' (#51) from fix/settings-write-debounce into main
ci / fork-checks (push) Successful in 25s
ci / build (push) Canceled after 6m56s
2026-09-25 04:35:08 +00:00
jcoffey-dev 71ce11c57d Settings writes: wait for a burst to settle before reloading
ci / fork-checks (pull_request) Successful in 56s
ci / build (pull_request) Successful in 12m43s
A cluster rehearsal sent ten x:<Object>/set requests at once and got
ten full reloads on every node. #39's coalescing only joined writes
that queued behind a running reload, but the requests reached the
server about 33 ms apart and a reload takes tens of milliseconds, so
none overlapped one.

A full reload after a registry write now waits for writes to settle:
75 ms after the last one, and at most 250 ms after the first it
covers, so a steady stream still reloads at least four times a
second. 75 ms is a little over twice the gap the rehearsal saw between
requests. A single write pays it once: in the tests a settings write
takes about 140 ms instead of 60. The reload runs in a task of its
own, so a request that goes away doesn't cancel it for the others.
Each write takes the result of the first reload that started after it
was stored (the gate keeps the last 64 results), so applied true or
false still describes the reload that covered that write.

The 33 ms gap was a queue on the server, not password hashing: Basic
credentials are cached per Authorization header, so they are checked
once. Every authenticated HTTP request counted itself against the
account's rate limit by incrementing one counter per account in the
in-memory store, so parallel requests from one account queued on that
key: a row lock on PostgreSQL (a few round trips to the database
each) and conflict retries with a 50-300 ms backoff on RocksDB. An
account with the unlimitedRequests permission (administrators, by
default) passes the rate and concurrency limits anyway, so its
requests are no longer counted. Ten parallel Core/echo calls as the
admin now finish in 1-4 ms; before, they finished one after another
over 20 ms on a local PostgreSQL and 300-450 ms on RocksDB. Other
accounts still count every request.

system::auto_reload::settings_reload_tests: ten concurrent writes now
take one reload (the gate counts them; at most two allowed), all are
applied: true and in the running settings, and a single write takes
exactly one reload. RocksDB and PostgreSQL, 1 reload in 141-196 ms.
With the old behavior (no wait, requests counted) the same writes
took 5 reloads; without the wait but with the rate fix, 2.
cluster::broadcast (3 nodes, PostgreSQL + NATS) and system::reload
still pass.
2026-09-24 21:15:44 -07:00
jcoffey-dev 51b159a1a2 Merge pull request 'Tracers whose settings change start over on reload' (#50) from fix/tracer-live-reload into main
ci / fork-checks (push) Successful in 24s
ci / build (push) Successful in 34m49s
2026-09-25 03:57:52 +00:00
jcoffey-dev a891667149 Tracers whose settings change start over on reload
ci / fork-checks (pull_request) Successful in 47s
ci / build (pull_request) Successful in 3m49s
A cluster rehearsal moved a Log tracer to another directory: the write
was reported x:settingsReload applied:true, but the tracer kept writing
to the old file until a restart. Telemetry::update only refreshed each
running tracer's events, level and lossiness; a tracer's own settings
(path, prefix, rotation, format, endpoint, headers, ...) stayed as built.

Each tracer now carries a hash of the registry object it was built
from, less the fields that change in place. The reload compares it with
the running tracer's: unchanged ones are updated in place as before,
changed ones are started over, new ones started and removed ones
stopped. Only tracers this server started are removed; upstream removed
every subscriber not in the settings, which also cut off live-tracing
streams on each reload.

Starting over is a swap in the collector, so no event is lost or
written twice: a subscriber registered under a running one's id
replaces it between two collection passes. The old one's batch is sent
first (what its full channel can't take moves to the new one), and
dropping it closes its channel, so its task writes what is queued and
ends. Per tracer kind:

- Log: a tracer started over on the same files (rotation or format
  changed) waits for the old one to finish, so lines don't interleave.
- Webhook: the task held a sender of its own channel for retries, so
  it never ended; retries now use a weak sender, and pending events are
  posted when the channel closes.
- OpenTelemetry: pending logs and spans are exported when the channel
  closes instead of dropped, and a span that was open across the swap
  is exported by the new tracer with the events it saw.
- Console and journal: nothing kept between batches.
- Trace history: built from the tracing store, which takes a restart,
  so it is never started over.

No kind needs a restart, so x:settingsReload doesn't gain one.

system::tracer_reload::tracer_reload_tests (new): a Log tracer created
over JMAP writes to its directory; its path is changed over JMAP while
2000 numbered events are emitted; after the reload, events land in the
new file and not the old one, each numbered event is in exactly one of
the two files, and a destroyed tracer writes nothing. On main the new
file never appears.
2026-09-24 20:52:46 -07:00
jcoffey-dev 59e631eded Merge pull request 'Every node records DMARC and TLS results for the aggregate reports' (#47) from fix/front-node-dmarc into main
ci / fork-checks (push) Successful in 1m27s
ci / build (push) Successful in 40m18s
2026-09-25 01:46:19 +00:00
jcoffey-dev 716800d681 Merge pull request 'Publish: accept tags on release/* branches for hotfix releases' (#48) from ci/publish-release-branches into main
ci / fork-checks (push) Successful in 19s
ci / build (push) Canceled after 7m17s
2026-09-25 01:38:56 +00:00
jcoffey-dev 4b85113262 Publish: accept tags on release/* branches for hotfix releases
ci / fork-checks (pull_request) Successful in 20s
ci / build (pull_request) Successful in 7m16s
The publish workflow only built a tag whose commit is on main. That keeps
every image tied to reviewed code, but it means production can only get a
fix together with everything that has landed on main since its release.

A tag on a release/* branch is now accepted too. A hotfix branch starts at
an earlier release tag, takes fixes through pull requests into it (so the
code is still reviewed and CI-tested before it is tagged), bumps
brand_version! and is tagged there. The tag must still equal
v<brand_version!>, and the step prints which branch it was found on.

A tag runs the workflow file from its own commit, so a hotfix branch that
starts before this change needs this commit cherry-picked onto it before
its tag is pushed.
2026-09-24 18:31:24 -07:00