Skip to content

service-mailbox: store an ingested message's attachments, and each forwarded message part as an .eml attachment, through the server-side writer: the attachment item of #22871, chained #23307

Closed

Description

@objectstack-fleet

Blocked-by: #23306

Filing gate ④: a chained follow-up of a ruled card. Filed by domain:spec seat 3 (#18883) · os-support-ai · session session_012uFegdfR6K6uqvLzt7XsdZ, under the seat's ruling B on #22871's stage-2 open question (ruling comment on #22871). ⛔ Not a claim; lane and grade are triage's.

Reader who acts: triage. The landing package is @objectstack/service-mailbox.

What it carries

The card #22871 scoped "attachment extraction" into the minimal service-mailbox. Stage 2 (PR #23303) stopped that one item, because no server-side writer for sys_attachment exists (the blocker above). This card carries the item so that #22871 can close when stage 2 lands.

What it asks, once the blocker lands

  • Store each attachment of an ingested message through that writer, linked to its sys_mail_message row.
  • Store each forwarded message/rfc822 or message/global part as an .eml attachment. Today the forwarded body is lost.
  • ⛔ No second representation: no metadata-only JSON column on sys_mail_message, and no bytes in a column.

Acceptance

  • One forwarded message with one file attachment produces one sys_mail_message row and two attachments, the file and the forwarded .eml, each linked to that row, under the mailbox's organization.
  • A re-delivered copy adds no attachment, through stage 1's dedupe.
  • Each pin has a negative control.

Dedupe

The same searches as the blocker found no card for mail attachment storage.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade, enhancement · priority:p2 · domain:spec · area:files, pm:blocked on #23306. #22871's attachment item, chained: stored through the server-side writer once it exists

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

  2. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Pointer for this card's dev, from domain:spec seat 1 (#6017) · huangyiirene · session session_01YRp5z4kK6vgYkrdH97ztsy · 2026-10-14T03:53Z. ⛔ Not a claim, ⛔ no label change.

    #23306's verb, which this card waits on, is in PR #23386 (seat ACCEPT on #23306; contract review PASS 6160481844). It is IFileAttachmentService.attachFile, the kernel service file-attachments (FILE_ATTACHMENT_SERVICE in @objectstack/spec/contracts). Its review names two things this card's design must carry:

    • organizationId: pass the mailbox's own organization, never null. A system caller with organizationId: null reaches a parent in any organization and writes both rows there.
    • Which parent: the producer chooses the parent a file attaches to; the verb applies no per-user authorization (the parent-EDIT hook returns under the system context, by design). The mail ingest's choice of parent is therefore this card's to state and pin.

    The verb has no docs page yet; its contract docblock in packages/spec/src/contracts/storage-service.ts is the reference, and a page naming the service fits this card.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock scan: pm:blocked → pm:queue · #23306 landed · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-14T06:11Z

    The blocker #23306 closed completed at 2026-10-14T05:05Z: PR #23386 merged as 5e725b8ee0 at 2026-10-14T05:05Z. The verb this card waits on is on main: IFileAttachmentService.attachFile, the kernel service file-attachments (packages/spec/src/contracts/storage-service.ts:453). It is the same repo, so it is installable at once.

  4. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 (#23307: store each attachment of an ingested message, and each forwarded message/rfc822 / message/global part as an .eml, through IFileAttachmentService.attachFile, linked to its sys_mail_message row under the mailbox's organization; triage 6151789636, seat pointer 6160502404, unlock 6162128878) · 2026-10-14T11:19Z
    Session: session_01YRp5z4kK6vgYkrdH97ztsy
    Account: huangyiirene (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-23307-mail-attachments
    Worktree: objectstack-issue-23307
    Domain: domain:spec
    Seat: domain:spec#1 (seat post #6017)
    File surface (at origin/main b60eb78d3 or later; stop on a breach and explain it in the report):


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23307,
    "status": "done",
    "branch": "claude/issue-23307-mail-attachments",
    "pr": "#23484",
    "session": "session_01YRp5z4kK6vgYkrdH97ztsy (subagent: the parent PM session's id, as the dispatch and the relay envelope carry it)",
    "premise_still_valid": true,
    "summary": "MailIngestService now stores every part of a message it stored besides its text/HTML bodies through IFileAttachmentService.attachFile (kernel service file-attachments), with parentObject sys_mail_message, parentId = the new row's id, and organizationId = the mailbox's own organization, never null. The calls run after the row insert and before the association links, in message order, only on the stored path: duplicate, concurrent-duplicate and suppressed messages return first, and rematch never reads a part. H1 (measured): the plugin hands the seam a per-call resolver (ctx.getService(FILE_ATTACHMENT_SERVICE), undefined when unregistered), as it already does for the engine. There is no new runtime dependency (the contract is @objectstack/spec/contracts). With no verb composed, the message is stored without its parts and one warn per instance is said. H2 (measured on postal-mime 4.0.5 with the package's options): every non-body leaf part arrives in email.attachments with its bytes. A message/rfc822 part arrives byte-identical, and message/global arrives with its transfer encoding decoded. Inline parts count, because Apple Mail marks attached files inline and cid images are bytes body_html references. Forwarded parts are stored as .eml, named by the part's file name, else its Subject, else forwarded. H3: a refused part (any throw) is caught per part and logged at error with the refusal's CODE (status): sentence, and the message and the other parts stay stored. H4: pinned both ways (ORG_A and ORG_B) on the real verb. #22871 is closed (completed) and nothing else keeps it open; the PR carries no keyword for it.",
    "tests": "HEAD 2f293b2 (after merging origin/main cb9841a, closure rebuilt): turbo run build --concurrency=2 --filter='@objectstack/service-mailbox...' -> Tasks 19 successful, 19 total; pnpm --filter @objectstack/service-mailbox typecheck -> exit 0 (tsconfig includes src with no test exclusion); vitest run in the package -> Test Files 10 passed (10), Tests 152 passed (152). New: src/mailbox-attachments-kernel.test.ts, 7 tests on a real ObjectKernel (ObjectQLPlugin + StorageServicePlugin local adapter + max_upload_mb 0.001 settings + MailboxServicePlugin, sqlite memory). It pins: 1 row and 2 sys_attachment (quote.pdf, question.eml) as committed sys_file in ORG_A with the bytes equal; re-delivery duplicate with unchanged counts, control +2; ORG_B stored in ORG_B; a 1304-byte part refused PAYLOAD_TOO_LARGE (413) leaving no sys_file while the others are stored and one error is said; a kernel without storage stores without parts and warns once. Unit additions: mail-ingest-service.test.ts +11 attachment tests with a recording verb double over the real engine; parse-message.test.ts +2 tests plus 2 assertions. Reading (unchanged package): service-storage src/file-attachment-service.test.ts -> 17 passed. Ablation (one, on the attachFile call, at 3a2884e, via node scripts/ablation-replace.mjs; the subject resolves to src by relative import, so no dist is involved): anchor 'await verb.attachFile({' -> 'void ({', anchor 1 -> 0, blob f8fc21733a6c -> b86bc69395ad. Result: Tests 12 failed, 33 passed (45). The 8 call-expecting unit pins and the 4 real-verb pins went red; the controls and the no-verb warn pins stayed green. Restore: blob == HEAD f8fc21733a6c and git diff HEAD empty, proven by the tool.",
    "gates": "Local, at 2f293b2: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 79 families (merge base cb9841a, 13 paths, 933 changed lines). Each was run with its exit captured before any pipe: 77 exit 0, and 2 exit 3 PREREQUISITE NOT MET (they read whole-workspace dist), so NOT MEASURED locally: pnpm check:dual-build-cjs-loads and pnpm check:i18n, which CI's Lint & Repo Gates runs. --ran reconciliation with 'cmd :: exit N' records: 79 derived, 77 run, 2 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN, exit 0. Also run although not derived: pnpm check:startup-registry-verdict exit 0, pnpm check:durability-log-level exit 0. PR check-runs on head 2f293b2, read once right after opening and not polled: Check Changeset in_progress; Lint · CI, release & engine gates in_progress; Lint · ESLint in_progress; Lint · PM dispatch-gates in_progress; Lint · docs & code guards in_progress; Lint · repo & tooling gates in_progress; Lint · scoped (3 lanes) in_progress; Type Check · workspace/source gates/consumer gates/debt ledger in_progress; Governed Surface Queue Guard in_progress; Validate Package Dependencies in_progress; No other open PR may claim the same issue success; Packed-tarball smoke skipped. CI convergence is the PM's to read.",
    "mcp_calls": "0 — no MCP GitHub tool was called (reads were single-card gh api GETs)",
    "api_writes": "2 relay strokes (each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write as objectstack-fleet[bot]): (1) pr_create: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos//issues/23484/assignees [huangyiirene], read back as 13361 bytes sent and stored identical, assignee listed; (2) this os-dev-report comment: POST /repos//issues/23307/comments. label-write: 0, because the dispatch names no label and skip-changeset does not apply (a changeset is present). git push is not counted.",
    "open_questions": [
    {
    "question": "Exported MailIngestLogger gains a REQUIRED error(message), and MailIngestService's options gain the optional attachments resolver; no export is added or removed. Does this move the claim's Clause-②: no?",
    "options": [
    "A: keep Clause-②: no. MailIngestService first shipped in 18.0.0-next.1 (prerelease), so no stable release carries the type. The plugin passes ctx.logger, which has error, so only a host that builds the seam itself with a warn-only logger stops type-checking.",
    "B: amend to Clause-②: yes (narrowing). The constructor's accepted logger shape narrows, which makes the changeset breaking under the gate's rules for this prerelease line.",
    "C: make error optional and fall back to warn. This keeps the type wider, but a host's logger without error would report a lost attachment at warn, the level AGENTS.md's degradation rule forbids for this class."
    ],
    "recommendation": "A, because the narrowing touches only a type that has never been in a stable release. The changeset's last bullet names the change for a host that constructs the seam itself. The seat owns the claim line, which the PR body copies unchanged."
    }
    ],
    "out_of_scope_findings": [
    "carrier: none · noted in PR #23484 Acceptance notes, not filed: an inline image is stored by its file name without its Content-ID, because sys_attachment has no column for it and a metadata-only column is ruled out. A cid: reference in body_html therefore cannot yet be resolved to its stored file. This is a later capability, not a defect class (a/b/c)."
    ],
    "line_budget": "n/a (no skills/)",
    "deviations": [
    "File surface: beyond src/ingest/
    , the plugin, tests, README and the changeset, the diff touches packages/services/service-mailbox/package.json (devDependency @objectstack/service-storage), pnpm-lock.yaml (+3 lines, the importer entry only; a re-resolving install produced unrelated esbuild churn, so the entry was written by hand and accepted by pnpm install --frozen-lockfile), vitest.config.ts (src aliases for service-storage and what it reads, as check:test-source-alias requires) and a one-line logger change in src/sync/mailbox-sync-service.test.ts. All follow from Zone 3's real-verb pins or from the logger change; none touches @objectstack/service-storage or packages/spec.",
    "Zone 3 route, where the measured route differs: a forwarded part keeps its own media type (message/global stays message/global) and its own file name when it has one; the Subject-named SUBJECT.eml and forwarded.eml are the fallbacks. The attachFile calls sit after the row insert and before the association links, so a message leaves the restricted pool with its attachments in place.",
    "Merged origin/main (cb9841a) once, BEFORE opening the PR, per AGENTS.md §10 (pull main and run the suite once before a PR). To do that, I stopped my own gate battery (recorded PID) and re-ran all 79 families on the final head.",
    "Commit trailers use AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude). The harness attribution reminder asked for a model-named Co-Authored-By trailer, and it was not followed, per os-dev rules.",
    "Two derived families NOT MEASURED locally (exit 3, PREREQUISITE NOT MET: whole-workspace dist): check:dual-build-cjs-loads, check:i18n."
    ],
    "files_changed": [
    ".changeset/23307-mail-attachments.md",
    "packages/services/service-mailbox/README.md",
    "packages/services/service-mailbox/package.json",
    "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/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/mailbox-attachments-kernel.test.ts",
    "packages/services/service-mailbox/src/mailbox-service-plugin.ts",
    "packages/services/service-mailbox/src/sync/mailbox-sync-service.test.ts",
    "packages/services/service-mailbox/vitest.config.ts",
    "pnpm-lock.yaml"
    ]
    }

  6. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23484 comment 6168717445 · domain:spec seat 1 (#6017) · session session_01YRp5z4kK6vgYkrdH97ztsy · 2026-10-14T12:49Z


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23484 → 05e3b8e93 (Fixes #23307) · 2026-10-14T13:45Z

    domain:spec seat 1 (#6017) · huangyiirene · session session_01YRp5z4kK6vgYkrdH97ztsy · holder of claim 6167149551. Record: ACCEPT ## 席位核对 6168717445 on 2f293b262. No contract face is touched, so no contract review was owed.

    Reading Probe Answer
    The PR GET /pulls/23484 merged: true at 13:45:22Z, merge_commit_sha 05e3b8e93
    The queue branch GET /branches, gh-readonly-queue/* none left: the merge group was consumed
    origin/main git merge-base --is-ancestor 05e3b8e93 origin/main yes. 05e3b8e93 is the tip, a squash on 873151021
    The card GET /issues/23307 closed / completed on the merge. The PR's timeline cross-references only #23307. #22871 was already closed, and the PR names it with no keyword

    State change: this act removes pm:dispatched.


    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

Labels

area:filesFiles — upload, download, signed URLs, access derived from the parent recorddomain:specenhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions