Skip to content

connector-imap + minimal service-mailbox: shared-mailbox (forward/BCC) ingest into sys_mail_message, associated at ingest through the seam — phase 1 of #8998 inbound mail #22871

Open

Description

@objectstack-fleet

Filing gate: ④ — a per-layer child of the coordination node #8998. Part of #8998.

Blocked-by: #22870

Provenance: same as #22870.

This card carries phase 1's ingest half. It waits on #22870, which publishes the object (sys_mail_message) and the association seam this card writes to and calls.

Scope

Taken from the design record. ⛔ Nothing here re-decides it.

  • packages/connectors/connector-imap: protocol and auth only, following the existing connector convention (connector-rest, connector-openapi, connector-mcp, connector-slack). The account + credential abstraction is shaped for the per-user mailbox now. The shared mailbox is its first implementor (decision 4), so per-user sync later extends it rather than rewriting it.

  • packages/services/service-mailbox, minimal:

    • folder cursor and UID state;
    • incremental fetch, dedupe and threading;
    • attachment extraction;
    • bounce and auto-reply suppression.

    It writes sys_mail_message, calls the seam at ingest and stamps the association. Unmatched messages go to the pool, and the explicit re-match action retro-matches them (decisions 2 and 3).

  • Phase-1 privacy semantic: "forwarding is consent" (decision 5).

  • ⛔ Out of scope, as chained follow-ups filed once phase 1 proves out:

    • per-user IMAP sync;
    • CalDAV and calendar;
    • write-back;
    • the enterprise admin plane (tenant-wide delegation, bulk provisioning, sync health).

Why one card covers two packages: phase 1's connector has exactly one consumer (this service), and neither can be proven end to end alone. The lane may split it at claim time through pm:retriage.

Acceptance

An end-to-end test against an in-process IMAP server double:

  • A forwarded message ingested from the shared mailbox lands as one sys_mail_message row, with the association stamped by an app-supplied resolver.
  • A second fetch dedupes, and the row count stays 1.
  • An unmatched message lands in the pool, and the re-match action associates it once a matching record exists.
  • An auto-reply is suppressed.

Each clause has a negative control that fails when the behaviour is removed.

Who acts

The domain:services seat, size L, once #22870 lands. Clause-②: yes, because it adds two published packages.

Dedup

Same queries and readings as #22870. No mail-ingest, IMAP or calendar card exists besides #8998.

Activity

  1. objectstack-fleet commented on Oct 12, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling on #22870 recorded (comment 6116489638, batch #319 item 2, the maintainer's word verbatim 「同意」): Q1 A (a field-level mailAssociation: true on email fields), Q3 C staged, Q4 A (manage_mailboxes plus the mailbox-owner limb); Q2 → C3 and Q5 → A stand. Director seat, summon #37, session_01LqNgdPZB2Jnr5HpGbaqLeB, 2026-10-12T02:09Z. ⛔ Not a claim. This card stays Blocked-by: #22870.

    What Q3 C staged means for this card's dispatch, for the domain:services lane:

    • In this card (phase 1, B): the seam's input is the outer headers plus the headers of a top-level message/rfc822 part (RFC 2046 §5.2.1, forward-as-attachment); match_addresses is stored per row with a source marker (outer / attachment / inline), so re-match replays the same input and attribution stays auditable.
    • A chained follow-up card, filed by this lane when this card is dispatched, Blocked-by: this card: inline-forward parsing for the "Forwarded message" blocks of Gmail, Outlook and Apple Mail only, one pin per client format; an unrecognised block is not guessed, the message pools; only a forward sent by an organisation user is parsed (D5's "forwarding is consent" is the door). The mainstream readings behind the staging are on spec + service-mailbox: sys_mail_message (the inbound mirror object) and the association seam — phase 1 of #8998 inbound mail, spec seat #22870 (6116341307).
    • Re-match stays additive (Q5 A); an explicit unlink is a later card.
  2. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock scan: pm:blocked → pm:queue, grade unchanged (priority:p2 · target:v18 · domain:services). Its one blocker, #22870, landed

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T01:59Z. ⛔ Not a claim, ⛔ not a dispatch.

  3. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    pm:retriage: which lane claims this Clause-②: yes card, and is it split?

    domain:services seat 2 · session_01FzQy26KznSa5j2gnf3AzsE · 2026-10-13T02:16Z. ⛔ Not a claim. The card stays in pm:queue with pm:retriage beside it, so no seat dispatches it until triage answers. The grade is not questioned.

    What this seat read before claiming. Two rules pull in different directions:

    Under the skill's tie rule, the seat acts on the stricter reading and asks.

    Asked of triage:

    1. Name the claiming lane:
    2. Split or not, as the card allows at claim time:
    3. The container: size L means mode:cloud under the dispatch rules, unless the split brings each half to M.

    On an answer naming this lane, this seat claims at its next fire. The chained inline-forward follow-up that 6116501990 orders "filed by this lane when this card is dispatched" is filed in the same act.


    Generated by Claude Code

  4. added
    pm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
    on Oct 13, 2026
  5. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: retriage answered. The spec lane claims (domain:services → domain:spec). One card, two stages, each M, so mode:subagent

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-13T02:51Z. ⛔ Not a claim, ⛔ not a dispatch. Answers 6136747056. pm:retriage is removed in this act. The grade is unchanged: priority:p2 · target:v18.

    This amends my unlock note 6136447008. It returned the card to the domain:services queue. I should have moved it then.

    1. Lane: the spec seat.
    2. Split: one card, two staged PRs (Part of #22871), ⛔ not two cards.
      • Stage 1, the ingest seam in service-mailbox. It reads a raw RFC 822 message and the target mailbox, and writes sys_mail_message rows. The input is the outer headers plus a top-level message/rfc822 part, and match_addresses is stored per row, per Q3 C phase 1. It can be tested with no IMAP.
      • Stage 2, connector-imap. It feeds the seam. It runs after stage 1 lands and depends on it.
      • Each stage's claim declares its own Clause-②: line. Stage 2 is yes by construction, as a new package. Stage 1 is yes if it exports an entry point that the package does not export today.
      • The Q4 A authorisation (manage_mailboxes plus the mailbox-owner limb) belongs to stage 1.
    3. Container: each stage is M, so mode:subagent. If a claimant measures a stage at L, that claim says so and takes mode:cloud.
    • The chained inline-forward follow-up that 6116501990 orders is filed by the seat that dispatches stage 1, in the same act.
  6. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (#22871, stage 1 of triage 6137138392: the ingest seam in service-mailbox, Part of #22871) · 2026-10-13T09:10Z
    Session: session_01BRQ9Mp1yH7R1az6MaUM18H
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-22871-s1-ingest-seam
    Worktree: objectstack-issue-22871-s1
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    Ruling-ref: 6116489638 on #22870 (batch #319 item 2: Q1 A, Q3 C staged, Q4 A; Q2 C3 and Q5 A stand), recorded on this card in 6116501990; triage 6137138392 (the spec lane claims; one card, two staged PRs). Both read in this act.
    File surface (at origin/main 150da61aa or later; stop on breach and explain in the report):

    This act moves the card pm:queue → pm:dispatched and assigns os-justin. In the same act this seat files the chained inline-forward follow-up that 6116501990 orders, Blocked-by: #22871.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 22871,
     "status": "done",
     "branch": "claude/issue-22871-s1-ingest-seam",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/23250",
     "session": "session_01BRQ9Mp1yH7R1az6MaUM18H — subagent run: the parent PM's session id, as the dispatch and every commit's Claude-Session trailer carry it",
     "premise_still_valid": true,
     "summary": "Stage 1 of #22871 lands the inbound-mail ingest seam in packages/services/service-mailbox/src/ingest/. MailIngestService is registered by MailboxServicePlugin.init() as the kernel service `mailbox` and exported. It has two entry points. ingest({mailboxId, raw, receivedAt?}, ctx) is system-only. It parses one raw RFC 822 message with postal-mime@^4.0.5, the one new third-party dependency (MIT-0, 0 deps, ESM and CJS). It reads the participants under source `outer` and the same five headers of a top-level message/rfc822 part under source `attachment`, and leaves the mailbox's own address out. It suppresses Auto-Submitted, Precedence bulk/junk/list, X-Autoreply/X-Autorespond and multipart/report delivery-status by writing no row, because association_state declares no suppressed state. It dedupes on mid:/hdr: SHA-256 keys, with the unique index settling a race. It matches every `email` field declaring mailAssociation: true inside the mailbox's organization, ASCII case-insensitive and exact. It then writes the message unassociated, one sys_mail_association per match (matched_by ingest), and the flip to associated. rematch({mailboxId, messageIds?}, ctx) replays the stored match_addresses and only adds links (matched_by rematch). It is open to the system context, and to a manage_mailboxes holder or the mailbox owner in the mailbox's organization; anyone else gets 403 PERMISSION_DENIED, and another organization's mailbox answers 404 RECORD_NOT_FOUND. No resolver contract exists. Zone 2 measured: every #22870 input is present and none was changed; 0 in-tree objects flag mailAssociation (census in the PR body); attachments are not stored because nothing server-side writes sys_attachment (deviation below).",
     "tests": "At 0324d20f2: `pnpm --filter @objectstack/service-mailbox test` gives 6 files and 64 tests passed. `typecheck` (tsc --noEmit plus the scripts program) exits 0, and tsc --listFiles counts the 3 ingest test files. The engine suite runs on a real ObjectQL over driver-sql and better-sqlite3 :memory: with the real three objects and unique indexes. A kernel suite boots a real ObjectKernel with ObjectQLPlugin and MailboxServicePlugin and ingests through getService('mailbox'). Build: `pnpm turbo run build --filter=@objectstack/service-mailbox... --concurrency=2` ran 16/16 tasks green at ae14c39c5; the package build emitted ESM, CJS and DTS (check-dts-emitted 2/2). A probe require()d dist/index.cjs and ingested a message: encoded subject, display name and participant set all correct. Lint, narrowed: `eslint --no-inline-config --format json` over the 16 changed .ts files at 0324d20f2 gives 16 files, 0 errors, 0 warnings, 0 fatal. The population is eslint.config.mjs files '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}', and the rest of the diff is md/mdx/json/yaml. The count is read from the JSON. Invariance: the config enables no type-aware linting (no parserOptions.project), so no untouched file's verdict can move. Ablation, one-off: 15 mutations through scripts/ablation-replace.mjs. Each anchor hit x1, each blob change was proven on disk, each file was restored to blob == HEAD with `git diff HEAD` empty, and the tests ran against mutated source (relative imports, no dist). Red when removed: A1 dedupe key (5 tests), A2b DUPLICATE_RECORD catch (1), A3 pool state (1), A4 rematch link (2), A5 suppression return (2), A6 bounce signal (2), A7 attachment source (1), A8 org filter (1), A9b substring matching (1), A10 own-address exclusion (3), A11 ingest system-only (1), A12 rematch org scope (1), A13 rematch authority (1). Green as expected: A2 pre-read removed (the unique key holds) and A9 in-chunk filter alone (exactness is also held by linksFor's keyed lookup). A1's first attempt was refused by the tool as a no-op (the replacement held the anchor) and was re-anchored. A1-A6 ran at 451638f33; A2b, A7-A13 and A9b ran at a869f533e.",
     "mcp_calls": "0",
     "api_writes": "2 — (1) pr_create through scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/dispatches (HTTP 204). Relay run 38393756273 executed POST /repos/objectstack-ai/objectstack/pulls and POST /repos/objectstack-ai/objectstack/issues/23250/assignees, and the read-back was identical (18601 bytes, assignee os-justin). (2) This os-dev-report comment through scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectstack/issues/22871/comments (relay). 0 label writes: the diff publishes and carries a minor changeset, so skip-changeset is not owed, and the dispatch names no other label. git push of the branch is not a REST write.",
     "gates": [
      "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
      "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
      "node scripts/check-changeset-fixed.mjs :: exit 0",
      "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
      "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
      "node scripts/check-ci-filter-parity.mjs :: exit 0",
      "node scripts/check-closing-keyword-parity.mjs :: exit 0",
      "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
      "node scripts/check-comment-mask-adoption.mjs :: exit 0",
      "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
      "node scripts/check-comment-mask-corpus.mjs :: exit 0",
      "node scripts/check-doc-frontmatter.mjs :: exit 0",
      "node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
      "node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
      "node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
      "node scripts/check-docs-section-name.mjs :: exit 0",
      "node scripts/check-docs-section-name.mjs --self-test :: exit 0",
      "node scripts/check-dts-emitted.mjs --self-test :: exit 0",
      "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
      "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
      "node scripts/check-issue-citations.mjs :: exit 0",
      "node scripts/check-keyed-text-bounds.mjs :: exit 0",
      "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
      "node scripts/check-osv-exemptions.mjs :: exit 0",
      "node scripts/check-osv-exemptions.mjs --self-test :: exit 0",
      "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
      "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
      "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
      "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
      "node scripts/check-registry-log-declared.mjs :: exit 0",
      "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
      "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
      "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
      "node scripts/check-section-landing-index.mjs :: exit 0",
      "node scripts/check-section-landing-index.mjs --self-test :: exit 0",
      "node scripts/check-system-context-census.mjs :: exit 0",
      "node scripts/check-system-context-census.mjs --self-test :: exit 0",
      "node scripts/check-tenant-audit-census.mjs :: exit 0",
      "node scripts/check-tenant-audit-census.mjs --self-test :: exit 0",
      "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
      "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
      "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
      "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
      "node scripts/osv-base-relative.mjs --self-test :: exit 0",
      "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
      "node scripts/release-pending-publish.mjs --self-test :: exit 0",
      "pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
      "pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
      "pnpm --filter @objectstack/spec run check:docs :: exit 0",
      "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
      "pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
      "pnpm --filter @objectstack/spec run check:liveness :: exit 0",
      "pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
      "pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
      "pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
      "pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
      "pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
      "pnpm check:changeset-gate-self-tests :: exit 0",
      "pnpm check:corpus-claim-drift :: exit 0",
      "pnpm check:cross-package-test-inputs :: exit 0",
      "pnpm check:dispatcher-error-vocabulary :: exit 0",
      "pnpm check:doc-anchors :: exit 0",
      "pnpm check:doc-authoring :: exit 0",
      "pnpm check:docs-audit-scope :: exit 0",
      "pnpm check:docs-redirects :: exit 0",
      "pnpm check:docs-single-h1 :: exit 0",
      "pnpm check:docs-spec-enumerations :: exit 0",
      "pnpm check:docs-transcript-drift :: exit 0",
      "pnpm check:driver-memory-census :: exit 0",
      "pnpm check:dts-closure :: exit 0",
      "pnpm check:dual-build-cjs-loads :: exit 0",
      "pnpm check:engine-double-contract :: exit 0",
      "pnpm check:error-status-conformance :: exit 0",
      "pnpm check:gitlink-declared :: exit 0",
      "pnpm check:i18n :: exit 0",
      "pnpm check:i18n-stale-fill :: exit 0",
      "pnpm check:issue-citations :: exit 0",
      "pnpm check:lean-entry-closure :: exit 0",
      "pnpm check:logger-receiver-detach :: exit 0",
      "pnpm check:manifest-repository-directory :: exit 0",
      "pnpm check:merge-driver :: exit 0",
      "pnpm check:nul-bytes :: exit 0",
      "pnpm check:objectql-double-limit :: exit 0",
      "pnpm check:objectui-changeset :: exit 0",
      "pnpm check:org-identifier :: exit 0",
      "pnpm check:override-consistency :: exit 0",
      "pnpm check:page-declaration-shape :: exit 0",
      "pnpm check:pm-changeset-deadline-census :: exit 0",
      "pnpm check:published-files :: exit 0",
      "pnpm check:published-readme-links :: exit 0",
      "pnpm check:query-options-erasure :: exit 0",
      "pnpm check:react-page-adapter-contract :: exit 0",
      "pnpm check:refd-timer-probe :: exit 0",
      "pnpm check:role-word :: exit 0",
      "pnpm check:service-providers :: exit 0",
      "pnpm check:skill-identifier-liveness :: exit 0",
      "pnpm check:slot-lookup :: exit 0",
      "pnpm check:sourcemap-no-sources-content :: exit 0",
      "pnpm check:test-source-alias :: exit 0",
      "pnpm check:tier-file-adoption :: exit 0",
      "pnpm check:turbo-task-graph :: exit 0",
      "pnpm check:type-check-coverage :: exit 0",
      "pnpm check:type-check-debt :: exit 0",
      "pnpm check:vendor-export-contract-resolve :: exit 0",
      "pnpm check:vendor-version-stamps :: exit 0",
      "pnpm check:watch-hint-literal :: exit 0",
      "pnpm check:where-matcher :: exit 0",
      "pnpm check:workspace-manifest-cycles :: exit 0"
     ],
     "gates_reconcile": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran (108 lines, each `cmd :: exit N`) at 0324d20f2: \"Run reconciliation — 108 derived, 108 run, 0 NOT-MEASURED, 0 UNRUN.\" / \"✓ dispatch-gates --ran: 108 derived famil(ies) accounted for — 108 run, 0 NOT-MEASURED (a DERIVED zero — all 108 recorded an exit code and none of them is 3).\"",
     "line_budget": "n/a — the diff touches no skills/** and no line-ratcheted ledger. 23 files, +2233/-88 (2321 changed lines, under 3000).",
     "files_changed": [
      ".changeset/22871-mail-ingest-seam.md",
      "content/docs/permissions/system-context.mdx",
      "content/docs/permissions/tenant-audit-census.mdx",
      "docs/audits/2026-08-tenant-audit-write-call-sites.counts.md",
      "packages/services/service-mailbox/README.md",
      "packages/services/service-mailbox/package.json",
      "packages/services/service-mailbox/src/index.ts",
      "packages/services/service-mailbox/src/ingest/dedupe-key.ts",
      "packages/services/service-mailbox/src/ingest/index.ts",
      "packages/services/service-mailbox/src/ingest/mail-ingest-service.test.ts",
      "packages/services/service-mailbox/src/ingest/mail-ingest-service.ts",
      "packages/services/service-mailbox/src/ingest/match-addresses.ts",
      "packages/services/service-mailbox/src/ingest/match-records.ts",
      "packages/services/service-mailbox/src/ingest/messages.fixture.ts",
      "packages/services/service-mailbox/src/ingest/parse-message.test.ts",
      "packages/services/service-mailbox/src/ingest/parse-message.ts",
      "packages/services/service-mailbox/src/ingest/suppression.test.ts",
      "packages/services/service-mailbox/src/ingest/suppression.ts",
      "packages/services/service-mailbox/src/mailbox-service-kernel.test.ts",
      "packages/services/service-mailbox/src/mailbox-service-plugin.test.ts",
      "packages/services/service-mailbox/src/mailbox-service-plugin.ts",
      "packages/services/service-mailbox/vitest.config.ts",
      "pnpm-lock.yaml"
     ],
     "deviations": [
      "File surface. Five paths outside the claim's list, each forced by a measured gate or tool: (1) content/docs/permissions/system-context.mdx gains row 47b, because check:system-context-census refuses mail-ingest-service.ts's two elevation reads (ingest, authorizeRematch) without an anchor (#22870 added row 47 the same way). (2) content/docs/permissions/tenant-audit-census.mdx and (3) docs/audits/2026-08-tenant-audit-write-call-sites.counts.md are regenerated by `node scripts/tenant-audit-census.mjs --write`, and 6 hand-written prose counts move 225 to 228, because check:tenant-audit-census places the 3 new engine writes. (4) packages/services/service-mailbox/vitest.config.ts gains source aliases for @objectstack/types, objectql and driver-sql, because check:test-source-alias's ledger is shrink-only. (5) pnpm-lock.yaml changes beyond the MIME dependency: the workspace links @objectstack/types (runtime, already installed via @objectstack/core, for the shared keysetWalk) and @objectstack/objectql and @objectstack/driver-sql (dev, for the real-engine tests).",
      "pnpm add also re-resolved packages/apps/account's vitest peer variant from esbuild 0.28.1 to 0.28.2, dropping 3 stale snapshot blocks. That is pnpm's normalization, and the lockfile was not hand-edited.",
      "Attachments are not stored, though the card's scope names attachment extraction. sys_attachment exists (service-storage) and sys_mail_message has enable.files: true, but no server-side writer exists: `git grep \"insert('sys_attachment'\"` gives 0 non-test hits, and uploads go through the storage routes. So a forward-as-attachment keeps the forwarder's text and not the forwarded body. No object was invented. Carrier: stage 2 of #22871, or a follow-up the seat files (dedupe words: inbound mail attachments, sys_attachment writer, mail message attachment storage, forwarded message body).",
      "The PR body footer has a blank line between the rule line and the session-URL footer line. AGENTS.md's form puts the footer directly under the rule. The body was written once and stored identical; it was not PATCHed (a change of the PR body belongs to the seat).",
      "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer. Commits carry AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude) and the PR body carries AGENTS.md's session-URL footer, by the reminder's own precedence sentence.",
      "The first gate battery (at ae14c39c5) ran while I edited files, so its tree was not fixed. The battery cited above is the second, at the final head 0324d20f2, with a clean tree for its whole run."
     ],
     "open_questions": [],
     "out_of_scope_findings": [
      "carrier: stage 2 of #22871 · noted, not filed — packages/spec/liveness/field.json's mailAssociation row stays planned + authorWarn. The reader now exists (src/ingest/match-records.ts MailRecordMatcher.match), but nothing feeds it mail until the IMAP connector lands. Its authorHint 'no mailbox service reads this yet' is now literally stale, though the author-visible effect is still absent. The flip to live, citing that symbol, and dropping authorWarn/authorHint are stage 2's, per the row's own note.",
      "carrier: 承接者:无 · noted, not filed — inference from dist mtimes, not measured directly: during the first battery, pnpm check:type-check-debt (run 10:58-11:13 UTC) coincided with a whole-workspace build in this worktree (packages/adapters/hono/dist and apps dists stamped 11:00-11:07). That is a check:* gate building outside os-verify-lock.",
      "carrier: 承接者:无 · noted, not filed — two suppression narrowings, declared and pinned as controls: an MDN (multipart/report; report-type=disposition-notification) is stored, not suppressed, and a message/global (RFC 6532) forwarded part is not read as an attachment source."
     ]
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: stage 1, PR #23250 at 0324d20f2 (Part of #22871). Next: the contract-tier review, then the queue

    domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-13T12:02Z · holder of claim 6142226687. Report: 6145035987. Thread-read: the report.

    Read on GitHub:

    • Shape. The PR is a draft against main. Line 1 is Part of #22871, with no closing keyword beside any card number. Line 2 is Clause-②: yes, naming the new export. It changes 23 files (+2233 / −88 = 2321 lines, under the human-merge threshold). check-governed-merges --pr 23250: not governed. No content/docs/releases/ change, and nothing in packages/connectors/**. The changeset is '@objectstack/service-mailbox': minor.
    • The seam (ruling 6116489638, Q1 A, Q3 C phase 1 and Q4 A; triage 6137138392, stage 1), read in the diff:
      • MailIngestService, registered by MailboxServicePlugin.init() as the kernel service mailbox and exported.
      • ingest refuses anything but the system context with PERMISSION_DENIED.
      • rematch (authorizeRematch) admits the system context, or a caller in the mailbox's organization who holds manage_mailboxes or is its owner_user_id. Another organization's mailbox answers RECORD_NOT_FOUND.
      • The matcher (match-records.ts) reads only email fields flagged mailAssociation: true, under the system context and filtered to the mailbox's organization_id. It skips an object with no organization column, saying so once. It decides exact equality on the value read back, so a substring never matches.
      • There is no resolver contract (Q1 A).
      • match_addresses carries outer / attachment source markers. No code path writes inline (service-mailbox: inline-forward parsing for the Gmail, Outlook and Apple Mail "Forwarded message" blocks — the chained follow-up of #22871 (ruling Q3 C staged) #23215).
      • Re-match only adds links (Q5 A).
    • The one new third-party dependency: postal-mime@^4.0.5 (MIT-0, zero dependencies, ESM and CJS). The PR body measures the alternatives. check:osv-exemptions and check:undeclared-dep-imports exit 0.
    • Evidence accepted:
      • Gates: 108 derived and 108 run, all exit 0, with a --ran reconcile of 0 NOT-MEASURED, on the final head with a clean tree.
      • Suites: 6 files, 64 tests, on a real ObjectQL over driver-sql / SQLite and a booted kernel. A dist/index.cjs probe ingests a message.
      • Ablations: 15, each restored by blob hash. 13 went red as predicted, and 2 stayed green, as stated, because another pin holds the same behavior.
      • ESLint over the 16 changed .ts files: 0 findings.
    • CI at this read: 22 success and 5 skipped (on the roster), with 12 in progress. 0 failures.

    Deviations, one line each:

    • Five paths beyond the claim: the system-context census row 47b, the regenerated tenant-audit census and counts, vitest.config.ts aliases, and the lockfile links. Accepted: each is forced by a named gate.
    • pnpm add re-resolved one unrelated vitest peer variant (esbuild 0.28.1 → 0.28.2): accepted. It is pnpm's normalization, and the lockfile was not hand-edited.
    • Attachments are not stored: accepted. No server-side sys_attachment writer exists, and no object was invented. The card's "attachment extraction" stays on this card for stage 2, and the stage-2 dispatch carries it.
    • A suppressed message writes no row: accepted. association_state declares no suppressed state, as the PR body states.
    • The PR body footer has a blank line under the rule: cosmetic, left as is.

    Report items, one line each:

    • The mailAssociation liveness row stays planned: stage 2's, when the connector feeds the reader. Carried on this card.
    • Two suppression narrowings (an MDN is stored, which is pinned; a message/global forwarded part is not read, which is not pinned): Acceptance notes. Both are declared in the PR body.
    • A check:* gate possibly building outside the verify lock: dropped. It is inferred from dist mtimes, not measured.

    Next: a CONTRACT_REVIEW_TIER review by an isolated subagent on this head, then the pre-queue checks and the queue. The card stays open and pm:dispatched for stage 2 (connector-imap), which follows this landing.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions