Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@

## The thing that decides everything

**Seven shared thoughts exist in the world.** That is the entire live corpus.
**No shared thoughts exist in the world.** That is the entire live corpus.

It briefly held eight, all of them written here to prove a path worked, and on
2026-09-06 they were removed (`RESONANCE_PURGE_CORPUS`, `ops/DEPLOY.md`) — not
because they were in the way, but because the first person to arrive cannot tell
a test from a stranger, and would have been introduced to one.

Everything below is secondary to that, and it is worth saying plainly because
the project has spent most of its effort on the other side. The engine now has
eighteen benchmark families, five verdicts, thirteen thresholds, four policy
versions and seven ADRs. The corpus has seven thoughts. A matcher with nobody
versions and seven ADRs. The corpus has nothing in it. A matcher with nobody
to match is not a product, however good the matching is — and the matching is
now good enough: on 2026-09-06 it found a genuine cross-domain twin between two
people who had never met.
now good enough: on 2026-09-06, before the corpus was emptied, it found a
genuine cross-domain twin between two people who had never met.

There is an irony worth naming. The thought that produced that match was about
a registry of employer conduct, and its author had already reasoned out the
Expand Down
2 changes: 2 additions & 0 deletions ops/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ dependency is the PostgreSQL driver (`psycopg[binary]`), installed by the
| browser origin allowlist | `--origin https://your.host` (repeatable) | must be the **exact** `https://` origin browsers will use; this is the CSRF/Origin check. Add a second `--origin` for a platform default host alongside a custom domain. |
| bind address / port | `--host 0.0.0.0 --port $PORT` | the image reads `PORT` from the platform |
| retire unsigned accounts | `RESONANCE_PURGE_UNSIGNED=report` counts and prints; `=1` carries it out. Tombstones every session whose owning account has no verified sign-in behind it, and revokes those accounts. `RESONANCE_PURGE_KEEP=<id>[,<id>…]` spares named sessions or accounts. A signed-in account is never touched, and a second run finds nothing left to do. Run `report` first, read the counts in the deploy log, then `=1`, then unset. | one-shot operator action |
| empty the corpus | `RESONANCE_PURGE_CORPUS=report` counts and prints; `=1` carries it out. Removes every thought, every standing-search alert, every introduction, every conversation and every shared topic — and leaves accounts, sign-ins and OAuth client registrations alone, so nobody signs in again and no connected MCP client re-authorizes. That is the whole reason it exists rather than `python3 -m src.persistence --db <DSN> reset`, which also wipes both. It takes no exceptions: `RESONANCE_PURGE_KEEP` is **refused**, not ignored — removing named thoughts is `RESONANCE_PURGE_SESSIONS`. Run `report` first, read the counts in the deploy log, then `=1`, then unset. Idempotent. | one-shot operator action |
| remove named thoughts | `RESONANCE_PURGE_SESSIONS=<session id>[,<id>…]` tombstones exactly the sessions named and nothing else, and retracts the standing-search alerts on **both** sides of each — the owner's and the one recorded for the person on the other end. Every id is reported with what happened to it, including ids that do not exist, so a run that did less than intended is visible in the log. Idempotent. Unset it after the deploy. | one-shot operator action |
| label encoder | `RESONANCE_EMBEDDER=<directory>` holding `tokenizer.json` and `onnx/model_quantized.onnx` (multilingual-e5-small, exported to ONNX; ~135 MB) plus the `onnxruntime` and `tokenizers` packages | **recommended.** Without it the semantic layer is the hand-written English lexicon, which is blind to most real vocabulary and to every other language: the same trip described twice in different words came back "not a resonance". With it, each label is embedded locally on the CPU (about 6 ms a pair, cached), and the cosine adds relatedness the lexicon could not see; structure, contradiction and the verdict are unchanged. The server refuses to start if the variable names a directory it cannot load, and `/api/product/health` reports `engine.label_encoder`. Build the image with `--build-arg RESONANCE_EMBEDDER_MODEL=Xenova/multilingual-e5-small` to bake the model in. |
| sign-in providers | `RESONANCE_AUTH_GOOGLE_CLIENT_ID` / `RESONANCE_AUTH_GOOGLE_CLIENT_SECRET`, and/or `RESONANCE_AUTH_GITHUB_CLIENT_ID` / `RESONANCE_AUTH_GITHUB_CLIENT_SECRET` | **required for a real deployment.** Setting either pair turns on sign-in, and sign-in then becomes the *only* way an account is created: `POST /api/product/guest` answers `403 sign_in_required`, and the OAuth consent page offers no anonymous option. With neither pair set the pseudonymous guest path stays on — that is the local-development and test configuration, not a production one. Callback URL to register with the provider: `https://<your origin>/auth/callback/google` (and `/auth/callback/github`). Scopes requested are only `openid email profile` / `read:user user:email`. |
| how mail leaves | This platform blocks outbound SMTP below its Pro plan, and its own documentation says so: "SMTP is only available on the Pro plan and above... Free, Trial, and Hobby plans must use transactional email services with HTTPS APIs", with Resend named as the recommended one. Measured here before that was found: ports 587, 465 and 25 all time out on IPv4, and IPv6 is off by default so every AAAA answers "network is unreachable". So set `RESONANCE_MAIL_API_KEY` (and `RESONANCE_MAIL_FROM`) and mail goes out over 443, the same door the site is served from. `RESONANCE_MAIL_API_URL` defaults to Resend's endpoint; Postmark and Mailgun differ only in field names. The `RESONANCE_SMTP_*` path still works where SMTP is allowed, and an API key wins over it wherever both are set. | |
Expand Down
71 changes: 65 additions & 6 deletions src/persistence/postgres_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,25 @@ def _connect(dsn: str):

_SAFE_SCHEMA = re.compile(r"[A-Za-z_][A-Za-z0-9_]{0,54}")

# What two people made together, in the order a delete may walk them: rows that
# reference a workspace before the workspace, and messages before the channel
# that holds them. Distinct from the corpus (`sessions`) and from the account
# (`users`, `oauth_grants`), which is why `reset()` names all three and
# `delete_connections()` names only this one.
CONNECTION_TABLES = (
"workspace_contributions",
"workspace_activity",
"workspace_links",
"workspace_artifacts",
"workspace_tasks",
"workspace_notes",
"workspace_members",
"workspaces",
"messages",
"channels",
"intros",
)


class PostgresRepository:
backend_name = "postgres"
Expand Down Expand Up @@ -218,9 +237,7 @@ def reset(self) -> None:
try:
for table in (
"oauth_grants",
"messages",
"channels",
"intros",
*CONNECTION_TABLES,
"idempotency_keys",
"audit_events",
"sessions",
Expand All @@ -233,6 +250,32 @@ def reset(self) -> None:
self._conn.rollback()
raise

def delete_connections(self) -> dict[str, int]:
"""Remove every introduction, channel, message and shared topic.

Accounts, sessions, sign-ins and OAuth client registrations are left
alone, so this empties what people made together without making anyone
sign in again or any connected client re-authorize.

No corpus generation bump: none of these tables is discoverable corpus
content, so the discovery index does not go stale when they change
(the same reason workspace writes never bump it -- see 0004).
"""
with self._lock:
try:
removed: dict[str, int] = {}
for table in CONNECTION_TABLES:
row = self._fetchone_map(f"SELECT COUNT(*) AS n FROM {table}")
count = int(row["n"]) if row else 0
if count:
self._execute(f"DELETE FROM {table}")
removed[table] = count
self._conn.commit()
return removed
except Exception:
self._conn.rollback()
raise

def close(self) -> None:
with self._lock:
self._conn.close()
Expand Down Expand Up @@ -389,6 +432,24 @@ def list_grants_for_user(self, kind: str, user_id: str) -> Sequence[Mapping[str,
self._conn.commit()
return [loads(row["record_json"]) for row in rows]

def delete_grants_of_kind(self, kind: str) -> int:
"""Every record of one kind, whoever it belongs to.

`delete_grants_for_user` cannot stand in for this: it walks the accounts
it is given, and a record whose owning account has since been revoked or
removed belongs to none of them, so it would be left behind and counted
by whoever looks next.
"""
with self._lock:
try:
cur = self._execute("DELETE FROM oauth_grants WHERE kind = ?", (kind,))
removed = int(cur.rowcount or 0)
self._conn.commit()
return removed
except Exception:
self._conn.rollback()
raise

def delete_grants_for_user(self, kind: str, user_id: str) -> int:
with self._lock:
try:
Expand Down Expand Up @@ -594,9 +655,7 @@ def import_payload(self, payload: Mapping[str, Any]) -> None:
with self._lock:
try:
for table in (
"messages",
"channels",
"intros",
*CONNECTION_TABLES,
"idempotency_keys",
"audit_events",
"sessions",
Expand Down
2 changes: 2 additions & 0 deletions src/persistence/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ def migrate(self) -> tuple[str, ...]: ...
def health(self) -> dict[str, Any]: ...
def get_corpus_generation(self) -> int: ...
def reset(self) -> None: ...
def delete_connections(self) -> dict[str, int]: ...
def close(self) -> None: ...

def put_user(
Expand Down Expand Up @@ -62,6 +63,7 @@ def put_grant(self, kind: str, key: str, record: Mapping[str, Any], *,
def get_grant(self, kind: str, key: str) -> Mapping[str, Any] | None: ...
def pop_grant(self, kind: str, key: str) -> Mapping[str, Any] | None: ...
def list_grants_for_user(self, kind: str, user_id: str) -> Sequence[Mapping[str, Any]]: ...
def delete_grants_of_kind(self, kind: str) -> int: ...
def delete_grants_for_user(self, kind: str, user_id: str) -> int: ...

def export_payload(self) -> dict[str, Any]: ...
Expand Down
137 changes: 137 additions & 0 deletions src/product/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
from src.product.notify import (Notifier, NoTransport, account_in_token,
self_test)
from src.product.service import LiveProductService, ProductError, StaleResultError
from src.product.standing import ALERT_KIND
from src.product.mcp_bridge import (
BridgeError,
INVALID_REQUEST,
Expand Down Expand Up @@ -205,6 +206,40 @@ def startup_purge_demo(runtime: "ProductRuntime", environ: Mapping[str, str] | N
return result


def _retract_alerts_for(runtime: "ProductRuntime", session_ids: set[str]) -> int:
"""Drop every standing-search alert that points at one of these thoughts.

An alert is a pointer to a pair of thoughts, so when either end is deleted
the alert is no longer about anything. `StandingSearch.retract_for_session`
reaches only the owner's own side -- the alert recorded for the *other*
person still names the deleted thought, and survives until whenever they
next look, when the liveness re-check finally drops it. That is right for a
person revoking one thought and wrong for an operator emptying a store: the
rows stay, and the next operator counts them.

Never fails the boot: an alert left behind is filtered on read anyway.
"""
repo = getattr(runtime.live, "repo", None)
if repo is None or not hasattr(repo, "list_grants_for_user") or not session_ids:
return 0
removed = 0
try:
for user in repo.list_users():
user_id = str(getattr(user, "user_id", "") or "")
if not user_id:
continue
for record in list(repo.list_grants_for_user(ALERT_KIND, user_id)):
mine = str(record.get("my_session_id") or "")
theirs = str(record.get("their_session_id") or "")
if mine not in session_ids and theirs not in session_ids:
continue
repo.pop_grant(ALERT_KIND, str(record.get("alert_key", "")))
removed += 1
except Exception as exc: # noqa: BLE001 - report, never abort the boot
print(f"standing search: retract failed ({exc.__class__.__name__}: {exc})")
return removed


def startup_purge_sessions(runtime: "ProductRuntime",
environ: Mapping[str, str] | None = None) -> dict[str, Any] | None:
"""One-shot operator action: ``RESONANCE_PURGE_SESSIONS=<id>[,<id>…]``
Expand Down Expand Up @@ -248,14 +283,17 @@ def startup_purge_sessions(runtime: "ProductRuntime",
runtime.live.delete_session(session_id, rebuild=False)
outcome[session_id] = "deleted"
deleted += 1
alerts = _retract_alerts_for(runtime, {k for k, v in outcome.items() if v == "deleted"})
if deleted:
runtime.live.rebuild_index()
result = {"requested": len(wanted), "deleted": deleted,
"already_deleted": sum(1 for v in outcome.values() if v == "already_deleted"),
"missing": sum(1 for v in outcome.values() if v == "missing"),
"alerts_retracted": alerts,
"outcome": outcome}
print(f"purge-sessions: requested={result['requested']} deleted={result['deleted']} "
f"already_deleted={result['already_deleted']} missing={result['missing']} "
f"alerts_retracted={alerts} "
f"({', '.join(f'{k}={v}' for k, v in outcome.items())}) "
f"(RESONANCE_PURGE_SESSIONS set; unset it after this deploy)")
return result
Expand Down Expand Up @@ -428,6 +466,104 @@ def startup_purge_unsigned(runtime: "ProductRuntime",
return result


def startup_purge_corpus(runtime: "ProductRuntime",
environ: Mapping[str, str] | None = None) -> dict[str, Any] | None:
"""One-shot operator action: empty the shared corpus, keep the accounts.

``RESONANCE_PURGE_CORPUS=report`` counts and prints; ``=1`` carries it out.

What a deployment accumulates before anyone real arrives is not data, it is
the residue of testing: thoughts written to exercise a path, the alerts
they raised against each other, the introductions accepted to prove
introductions work. Left in place it is indistinguishable, to the first
person who arrives, from a world where other people are thinking -- and the
resonance they are shown is with a test.

So this removes every thought, every standing-search alert, every
introduction, every conversation and every shared topic. It does not touch
accounts, sign-ins or OAuth client registrations: nobody signs in again and
no connected client re-authorizes, which is the whole reason this exists
rather than `python -m src.persistence ... reset`.

It takes no exceptions. ``RESONANCE_PURGE_KEEP`` is refused rather than
ignored, because an operator who sets it is expecting something to survive
and would otherwise find out afterwards; removing named thoughts is
``RESONANCE_PURGE_SESSIONS``, which does exactly that and nothing else.

Prints counts only -- never a topic, a label, a message or any thought
content. Idempotent: a second run finds nothing left to do.
"""
environ = os.environ if environ is None else environ
mode = (environ.get("RESONANCE_PURGE_CORPUS") or "").strip().lower()
if mode not in {"1", "true", "yes", "report", "dry-run"}:
return None
if (environ.get("RESONANCE_PURGE_KEEP") or "").strip():
print("purge-corpus: REFUSED -- RESONANCE_PURGE_KEEP is set and this action "
"takes no exceptions; use RESONANCE_PURGE_SESSIONS to remove named "
"thoughts, or unset RESONANCE_PURGE_KEEP to empty the corpus")
return {"refused": "RESONANCE_PURGE_KEEP is set"}
dry_run = mode in {"report", "dry-run"}

live = [row for row in runtime.live.repo.list_sessions()
if getattr(row, "deleted_at", None) is None]
session_ids = {str(getattr(row, "session_id", "") or "") for row in live}
session_ids.discard("")
connections = _count_connections(runtime)

result: dict[str, Any] = {
"dry_run": dry_run,
"sessions_to_delete": len(session_ids),
"connections_to_delete": connections,
"alerts_retracted": 0,
}
if not dry_run:
for session_id in sorted(session_ids):
runtime.live.delete_session(session_id, rebuild=False)
# Every alert, not only the ones this pass can reach through a live
# account: a corpus with no thoughts in it can hold no true pointer to
# one, and an alert whose owner was revoked belongs to no account to
# walk.
repo = runtime.live.repo
if hasattr(repo, "delete_grants_of_kind"):
result["alerts_retracted"] = int(repo.delete_grants_of_kind(ALERT_KIND))
else:
result["alerts_retracted"] = _retract_alerts_for(runtime, session_ids)
if hasattr(repo, "delete_connections"):
result["connections_deleted"] = dict(repo.delete_connections())
if session_ids:
runtime.live.rebuild_index()
print(f"purge-corpus: {'REPORT ONLY, nothing changed' if dry_run else 'applied'} "
f"sessions={result['sessions_to_delete']} "
f"alerts_retracted={result['alerts_retracted']} "
f"connections=" + ",".join(f"{k}:{v}" for k, v in sorted(connections.items()) if v)
+ " accounts_and_oauth_untouched=yes "
"(RESONANCE_PURGE_CORPUS set; unset it after this deploy)")
return result


def _count_connections(runtime: "ProductRuntime") -> dict[str, int]:
"""How much shared state two people made, by table, without removing it.

Read through the store's own connection so `report` and `applied` count the
same rows; a store that cannot answer reports nothing rather than guessing.
"""
repo = runtime.live.repo
tables = getattr(repo, "connection_tables", None)
if tables is None:
try:
from src.persistence.postgres_store import CONNECTION_TABLES as tables
except ImportError:
return {}
counts: dict[str, int] = {}
for table in tables:
try:
row = repo._fetchone_map(f"SELECT COUNT(*) AS n FROM {table}")
except Exception: # noqa: BLE001 - a table this store does not have
continue
counts[table] = int(row["n"]) if row else 0
return counts


def build_runtime(
db_path: str = ":ephemeral:",
*,
Expand Down Expand Up @@ -1514,6 +1650,7 @@ def main(argv: list[str] | None = None) -> None:
startup_purge_demo(runtime)
startup_purge_sessions(runtime)
startup_purge_unsigned(runtime)
startup_purge_corpus(runtime)
startup_assign_pseudonyms(runtime)
# R15C (#136): canonical OAuth for hosted MCP clients on this same origin.
# The startup log names the FIRST declared --origin; per-request metadata
Expand Down
2 changes: 2 additions & 0 deletions src/product/web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
startup_purge_demo,
startup_purge_sessions,
startup_assign_pseudonyms,
startup_purge_corpus,
startup_purge_unsigned,
startup_label_encoder,
)
Expand Down Expand Up @@ -1276,6 +1277,7 @@ def main(argv: list[str] | None = None) -> None:
startup_purge_demo(runtime)
startup_purge_sessions(runtime)
startup_purge_unsigned(runtime)
startup_purge_corpus(runtime)
startup_assign_pseudonyms(runtime)
# R15C (#136): canonical OAuth for hosted MCP clients on this same origin.
# Per request the issuer is re-derived from the host actually addressed
Expand Down
Loading