HT-3: clean-room protocol doc - #1
Closed
zaridan wants to merge 4 commits into
Closed
Conversation
Helpthread — open-source serverless helpdesk. CHARTER.md is the constitution: vision (FreeScout-inspired, Help Scout-caliber), AGPL-3.0 core + CLA + commercial first-party modules, clean-room provenance policy with reference hierarchy, architecture direction, staged roadmap. CLAUDE.md operationalizes it; provenance/ starts the mandated session log. No code precedes this document. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
Principle #1 anointed FreeScout as the sole behavioral oracle; the earned knowledge we draw on is plural (FreeScout, Chatwoot, RFCs, mail libraries). FreeScout stays as a named example — fixtures still come from black-box observation of the live instance. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
docs/clean-room-protocol.md operationalizes charter §3: session sides, reference hierarchy, session procedure, record templates, spec hygiene examples, contamination handling, quarantine registry, human-authorship bar. Consistency fixes from the proof pass: the 'nothing traceable to source-visible sessions' rule (protocol §8, CLAUDE.md, charter Phase 0) would have outlawed committing specs — now scoped to engine code with an explicit spec-side carve-out. Charter §3 gains a note on why FreeScout alone gets the categorical no-code statement. Example paths moved off lib/mail to avoid colliding with the quarantine's path names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
Help Scout is the experience North Star (founder ran support on it for years — now stated in the mission); FreeScout is demoted to its three real roles: proof of the self-hosted market, observable mail-semantics reference, and the legal-care object of the clean-room policy. Identity sections (intro, mission, architecture plugin-folder line, roadmap map line) no longer lead with FreeScout; the clean-room and licensing mentions stay — those are doing legal work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
zaridan
added a commit
that referenced
this pull request
Jul 12, 2026
…T-30) Store: deleteConversation() — a single UPDATE to status 'deleted', scoped to exclude already-deleted rows so a second delete reports a miss. No updated_at bump (never surfaced again; the row stays an exact record of its last live activity). Soft, permanently: rows and threads survive in storage (charter invariant #1) — every public path already treats 'deleted' as nonexistent, so only the flag flips. API: DELETE joins GET/PATCH on the item route (Allow header updated); 204 with an empty body via a new noContent() helper (still no-store); missing, already-deleted, and non-UUID ids are one identical 404 (§5 no-existence-leak). Per specs/api/agent-inbox-v1.md §4d (v1.1, HT-25). 349/349 tests, including post-delete invisibility across every endpoint, the §4a replay-vs-delete rule (a keyed replay of a successful send 404s after the delete), and proof the mail itself stays in storage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zaridan
added a commit
that referenced
this pull request
Jul 13, 2026
…sh (HT-34) All 8 findings incorporated. gmail-push: require email_verified=true on the Pub/Sub OIDC JWT; bind to the push envelope's exact subscription field; resolve emailAddress->mailbox (reject on mismatch) before reconciling; add a bounded daily history.list reconciliation sweep so dropped/delayed pushes cannot leave a mailbox stale. inbound-ingestion: attachment extraction is the pipeline's job (post-parse), not the transport's; make the ledger claim atomic (unique-key get-or-insert) and commit the store write + 'stored' outcome in one transaction (closes the concurrent-delivery and partial-failure double-create windows); loop suppression now requires a verifiable correlation (own Message-ID / valid own token) — sender identity alone never drops mail (invariant #1); 'Agent' capitalization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 14, 2026
zaridan
added a commit
that referenced
this pull request
Jul 14, 2026
A daily maintenance pass over active Gmail mailboxes (a plain injected-deps function like the HT-16 delivery worker; the SchedulerProvider/Vercel-cron schedule and the queue-consumer wiring are composition-root config, deferred to HT-43). Per active mailbox, failure-isolated: - Re-arm users.watch() (Gmail push silently expires every ~7 days) and store the fresh expiration via GmailWatchStateStore.setWatchExpiration, which updates watch_expiration ONLY and never the cursor (a renewal's fresh historyId is ahead of the stored cursor; overwriting it would skip un-reconciled mail — charter invariant #1). - Enqueue a bounded reconciliation sweep (one reconcile job per active mailbox onto HT-41's GMAIL_RECONCILE_TOPIC) — the safety net for dropped best-effort pushes. No dedupeKey; idempotent ingest makes the redundant work safe. The access token is acquired once per mailbox and reused for the single watch() call. The OAuth token layer (HT-38) owns needs_reconnect via invalid_grant; a valid-token watch() failure is transient. Re-arm and sweep are independent. New: MailboxStore.listActiveMailboxes, GmailWatchStateStore.setWatchExpiration, src/mail/gmail-watch-maintenance.ts. No migration. The reconciliation lease from gmail-push.md §6 is deferred to HT-48. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jul 16, 2026
zaridan
added a commit
that referenced
this pull request
Jul 17, 2026
…apshot (HT-50) CodeRabbit round 3: Gmail's HistoryLabelAdded schema carries the added labels in the record's TOP-LEVEL labelIds field, beside `message` — the embedded message's own labelIds is not guaranteed to be populated in history records. The folding logic was reading message.labelIds and OVERWRITING the tracked entry, so an omitted embedded snapshot would clobber a real ['SENT'] down to [] (losing the label the self-echo filter keys on and defeating the very INBOX-visibility hardening the fold exists for). Now merges (set union) the schema-guaranteed top-level delta into the tracked entry and never reads the embedded snapshot: merging can only ADD labels, so a later INBOX becomes visible without ever losing the earlier SENT. Still fails open — an absent delta leaves the snapshot intact, biasing toward ingesting, never toward dropping (invariant #1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zaridan
added a commit
that referenced
this pull request
Jul 17, 2026
…ening (HT-50) Review round 2 findings, all addressed: - Skip DRAFT-labeled history entries too (src/mail/gmail-reconcile.ts's isSelfEchoMessage), not just SENT-without-INBOX. Gmail autosaves an Agent's in-progress Gmail-UI reply as a new DRAFT-labeled message id on every pause, each surfacing in history.list before anything is sent; the prior filter left these to ingest as ghost/half-written "customer" messages. Safe in the drop direction with no ambiguity: genuine inbound mail can never carry the system DRAFT label. - Harden src/providers/adapters/gmail/history.ts's listAddedMessageIds against the (unverified-live, but plausible) case where Gmail records a self-addressed send's SENT and INBOX labels as two SEPARATE history records instead of one messagesAdded snapshot carrying both. Now also requests the labelAdded history type and folds a later labelsAdded record's label snapshot into an id already tracked via messagesAdded in the same listed window, so a split SENT-then-INBOX ordering can no longer cause a silent permanent drop (invariant #1). Live verification against the dogfood mailbox remains open and is called out in the module doc as such — this hardening does not depend on nor confirm the assumption, it removes the failure mode either way. - specs/mail/gmail-push.md: document the DRAFT skip; amend §4's cursor rule to scope "the batch" to messages actually handed to the ingest pipeline, closing the contradiction with §3's self-echo filter (which was already described as cursor-neutral but never reconciled against §4's literal wording). - Full `npx vitest run` on this branch: 44 files, 868 tests, all passing (exit 0) — the previously-unattributed 18 failures from review round 1 do not reproduce; no comparison against origin/main was needed since this run itself is clean evidence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zaridan
added a commit
that referenced
this pull request
Jul 17, 2026
…apshot (HT-50) CodeRabbit round 3: Gmail's HistoryLabelAdded schema carries the added labels in the record's TOP-LEVEL labelIds field, beside `message` — the embedded message's own labelIds is not guaranteed to be populated in history records. The folding logic was reading message.labelIds and OVERWRITING the tracked entry, so an omitted embedded snapshot would clobber a real ['SENT'] down to [] (losing the label the self-echo filter keys on and defeating the very INBOX-visibility hardening the fold exists for). Now merges (set union) the schema-guaranteed top-level delta into the tracked entry and never reads the embedded snapshot: merging can only ADD labels, so a later INBOX becomes visible without ever losing the earlier SENT. Still fails open — an absent delta leaves the snapshot intact, biasing toward ingesting, never toward dropping (invariant #1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zaridan
added a commit
that referenced
this pull request
Jul 20, 2026
…e install path (HT-79) (#98) * docs(modules): managed hosting becomes the mainline install path (HT-79) TJ walked the v1 manual self-host install as customer #1 (HT-82 dogfood) and rejected its friction: the designed loop is browse → buy → install → enable, not five hand-assembled seams. This revises marketplace-v1 to v1.1 with managed hosting as the default, the tarball flow demoted to a preserved self-host escape hatch. - specs/modules/marketplace-v1.md → v1.1: new hosting control plane (§3d, the sole trust domain holding both a license key and a per-desk provisioning grant, entirely outside the AGPL core); the buy → install → enable managed handoff via a one-time claim token redeemed by the control plane, never the desk (§3e); rewritten §5 (managed install/update/ uninstall mainline + self-host escape hatch §5.4); one-click in-place update for hosted instances, resolving the admin-ia "in-place update" conflict; hosted-instance lifecycle (lapsed/frozen keep running, refunded decommissions after a config-export grace, revoked immediately); §6/§7/§8/ §9/§10 updated. Entities, commerce/download/update-check APIs, and the artifact pipeline are unchanged. - CHARTER.md: amended by direct edit (§4 + §7 appendix, the PR #86 mechanism) — restates the credential/no-runtime-check/lapse invariants for managed hosting; leaves the §2 data-residency reconciliation to TJ. - specs/modules/catalog.md: §5 reconciling note (hosting machinery lives outside the core, so "no special machinery in the product" still holds). - SPEC-CHANGES.md: chosen-defaults and OPEN-needs-TJ lists for the lead. Invariants held verbatim: license = distribution credential only, zero runtime license checks in any module, lapse never stops running software (even when RIQ is the host), AGPL core never holds a marketplace credential and never calls the marketplace. Self-host escape hatch preserved and not weakened. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(modules): collect module runtime config browser-direct to the control plane (lead review fix) Lead-tier review finding on the managed-hosting revision: §5.1 promised 'no hand-entered env vars' but never said where operator-supplied module settings — most prominently the operator's own model API key, which charter §2 requires — come from in the managed flow. Added §3e 'Module runtime configuration': an install-time in-app form submitted browser-direct to the control plane (same never-transits-the-core rule as the claim token), stored in the control-plane vault, re-injected on update rolls; §5.1 reworded honestly; §10.13 extended — operator secrets in the vault are the credential face of the same data-residency call. Also drops SPEC-CHANGES.md from the branch: it is adjudication material for the PR description, not a permanent repo artifact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Operationalizes CHARTER.md §3 into a followable procedure: session-side declaration (spec/implementation/foundation), reference hierarchy, the never-read list, session checklist, literal templates for provenance/sessions.md entries and per-module manifests, spec hygiene good/bad examples, contamination handling ("if in doubt, it was contaminated"), quarantine registry, and the human-authorship bar.
Also fixes a real contradiction the proof pass caught: the "nothing in this repo's history may trace to a source-visible session" phrasing (in the draft's §8, CLAUDE.md, and the charter's Phase 0 bullet) would have outlawed committing specs — the entire point of spec sessions. All three now scope the rule to engine code with an explicit spec-side carve-out. Charter §3 additionally gains a parenthetical explaining why FreeScout alone gets the categorical no-code declaration (the maintainer's review question).
Includes this session's provenance/sessions.md entry per the protocol's own §4c.
Jira: https://resonantiq.atlassian.net/browse/HT-3
🤖 Generated with Claude Code
https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b