HiNet.

Documentation  /  Canonical record (HCR)

HiNet Canonical Record (HCR) — the shared normalization format

Status: v1 draft (2026-06-25). The single shape every connector normalizes into and the vault / RAG / training pipeline consume. Formalizes (and supersedes the prose of) SourceRecord in Track 2 spec §7. Implemented in app/osx/hinetd/hinetd/canonical.py (pydantic v2) so it is enforced, not just describedConnector.normalize(raw) -> CanonicalItem.

Design goal: lossless-enough that nothing meaningful from any source is dropped (anything without a typed home lands in source_meta), uniform-enough that downstream code never branches on the source.

1. The envelope — CanonicalItem

Field Type Notes
schema_version str e.g. "1.0". Bumped on breaking changes.
id str Stable global id — deterministic (see §6). Primary key in the vault.
source SourceType slack · telegram · whatsapp · gmail · gdrive · local · …
connection_id str the SourceConnection (one authorized account).
account_id str multi-account discriminator within a connector.
provider_id str | None original id (Slack channel:ts, Gmail Message-ID, Drive file id). None ⇒ derived (§6).
type ItemType message · email · file · doc · event.
conversation_id str | None the chat/channel/mailbox-thread/folder this belongs to. None for standalone files.
thread ThreadRef | None reply/thread linkage within the conversation (§4).
author Participant | None who produced it (sender / file owner).
participants list[Participant] everyone involved (chat members, To/Cc). Includes author.
timestamp datetime (UTC) canonical time. Always tz-aware UTC.
tz_offset_minutes int | None original local offset if known (else None).
source_epoch float | None raw source epoch seconds (audit / re-derivation).
edited_at datetime | None last edit, if observable.
deleted bool tombstone (source says it was deleted). Default false.
subject str | None email subject; optional title for docs.
content Content normalized body (§3).
attachments list[Attachment] files/media/links (§3).
reactions list[Reaction] optional; emoji + who.
labels list[str] flat tags: Gmail labels, Slack channel name, Drive folder path, etc.
origin Origin human-readable source location (account/workspace/channel/folder/file + deep link). Kept in full for RAG; generalized for training (§4b).
sensitivity Sensitivity connector default; owner-overridable.
classification Classification | None set by the pipeline, not the connector (fact/skill/voice/mixed/route).
content_hash str sha256 of normalized content for cross-source dedupe (§6).
source_meta dict lossless passthrough for source-specific fields with no typed home.
ingested_at datetime (UTC) when normalized.

2. Sub-models

Participantdisplay_name?, handles: list[Handle], person_id? (resolved canonical human; None = unresolved), is_self: bool (the owner). Connectors emit handles; the entity-resolver fills person_id later (§5).

Handlescheme: HandleScheme, value: str. The join key for identity resolution (e.g. two email handles with the same value across Slack + Gmail ⇒ same person_id).

Contenttext: str (the canonical body), format: "markdown"|"plain", parts: list[ContentPart] (optional structured breakdown), lang?. Conventions: HTML email → markdown; Slack/Telegram mentions resolved to @DisplayName in text (raw kept in a part); quoted replies rendered as markdown blockquotes.

ContentPartkind: "text"|"quote"|"code"|"mention"|"link"|"system", text, optional ref (e.g. mentioned handle, url). Lets RAG chunk smartly and training strip system noise.

Attachmentkind: "image"|"file"|"audio"|"video"|"link", filename?, mime?, size?, provider_ref? (source attachment id), vault_ref? (local path if imported), extracted_text? (OCR/text-extraction, opt-in).

ThreadRefthread_id: str (canonical thread key), reply_to? (provider id of parent), root_id?. The conversation_id lives on the envelope; ThreadRef is the finer reply graph.

Reactionemoji: str, by: list[Participant], count: int.

Origin — the human-readable where-from: account_label (e.g. me@work (Gmail)), space (Slack workspace / Drive "My Drive" or a shared-drive name), container (Slack #channel / Gmail label / Drive folder / chat title), item_name (file/doc/channel), path (full breadcrumb for display + retrieval), url (deep link back). See §4b.

3. Enums

4. Conversation / thread model (unifies chat + email)

4b. Provenance (Origin) — kept for RAG, generalized for training

Every record carries origin: the human-readable source location, not just the machine connection_id. It's load-bearing two ways: - RAG keeps it in full. Retrieval is metadata-filtered by origin ("only my #eng Slack", "only work Gmail", "files under Legal/Contracts", "since March") and answers cite it ("from your #eng Slack in Acme, 2024-01-15, Alice said…"). Multiple Slack workspaces / Google accounts stay distinguishable. - Training generalizes or strips it. DeltaView learns the owner's voice/skill, not where it came from — so origin is dropped or mapped to a coarse descriptor ("work chat", "personal email"). Provenance-specific facts stay in RAG, per the fact→RAG / skill→Δ split (Track 2 §4.3): the model never memorizes "this lives in #eng," while RAG can still cite it.

Origin per source:

source account_label space container item_name url
Slack Acme (Slack) workspace name #channel message permalink
Telegram Personal (TG) chat / group title t.me link (if any)
WhatsApp WhatsApp chat name
Gmail me@work (Gmail) label subject message deep link
Drive me@work (Drive) My Drive / Shared: <name> folder path file name webViewLink

Multi-space / multi-account is first-class: each Slack workspace and each Google account is a distinct account_label + connection_id, so "work Slack #eng" never collides with "side-project Slack #eng".

5. Identity & entity resolution

Connectors emit Participant.handles (never invent a person_id). A separate resolver (P2/P4) clusters participants into canonical people by strong handles (email, phone) first, then best-effort (display name within a source). is_self is set by matching the owner's known handles (configured at onboarding). Resolution is advisory metadata — records remain usable unresolved.

6. Stable ID + dedupe

7. Per-source mapping tables (source field → canonical)

Slack (conversations.history message) | source | canonical | note | |---|---|---| | ts | provider_id ({channel}:{ts}) | ts is the per-channel id | | channel | conversation_id | C/D/G id | | thread_ts | thread.thread_id / reply_to | flat msg ⇒ thread_id = ts | | user / bot_id | author.handles[slack_user] (+ email via users.info) | | | text (mrkdwn) | content.text | resolve <@U…>, <#C…|n>, <url|t> | | blocks/rich_text | source_meta.blocks | structured passthrough | | subtype | type=message+source_meta.subtype | service subtypes → ContentPart(kind=system) or skip | | edited | edited_at | | | reactions[] | reactions[] | | | files[] | attachments[] | url_private = provider_ref |

Telegram (export JSON message / Telethon Message) | source | canonical | note | |---|---|---| | id | provider_id ({chat}:{id}) | | | chat id/name | conversation_id | | | reply_to_message_id | thread.reply_to | | | from / from_id | author.handles[telegram_id, telegram_username] + display_name | | | text or text_entities | content.text (+ parts) | flatten entities to text + keep formatting in parts | | date_unixtime | source_epochtimestamp | prefer epoch over the ISO string | | forwarded_from | source_meta.forwarded_from | | | type=="service" | type=event or ContentPart(system) | joins/pins/etc. | | media + mime_type | attachments[] | metadata; file if opted in |

WhatsApp (Export Chat .txt) | source | canonical | note | |---|---|---| | [date, time] line | timestamp (+ tz_offset unknown) | parse locale variants (DD/MM vs MM/DD, 12h/24h, separators) | | Sender: | author.handles[whatsapp_phone or display_name] | number or name | | message text (multi-line) | content.text | join continuation lines | | <Media omitted> / IMG-….jpg (file attached) | attachments[] | placeholder; real media only if zip imported | | system lines (e2e notice, joins) | ContentPart(system) / type=event, author=None | | | (no native id) | synthesized provider_id | see §6 | | chat/export | conversation_id | per export file |

Gmail (users.messages resource) | source | canonical | note | |---|---|---| | Message-ID header | provider_id | stable across accounts (better than id for dedupe/threading) | | threadId | conversation_id | | | In-Reply-To / References | thread.reply_to / root_id | Message-ID graph | | From / To / Cc | author / participants[email] | | | Subject | subject | | | internalDate (epoch ms) | timestamp | prefer over Date header (tz/spoof) | | payload MIME: text/plain | content.text | if only text/html → convert to markdown | | labelIds[] | labels[] | | | parts w/ filename+attachmentId | attachments[] | | | full headers[] | source_meta.headers | passthrough |

Google Drive (files v3 resource) | source | canonical | note | |---|---|---| | id | provider_id | | | name | subject (title) | | | mimeType | type (doc if native/text-extracted, else file) | application/vnd.google-apps.* → export | | native export / local extract | content.text | Docs→md, Sheets→csv, Slides→text; PDF/docx→extracted | | parents[] | labels (folder path) / conversation_id=None | resolve ids → path | | owners[].emailAddress | author.handles[google_account/email] | | | modifiedTime | timestamp | RFC3339 | | md5Checksum | feeds content_hash (binary) | native docs hash exported text |

8. source_meta — the escape hatch

Any source field without a typed home goes here verbatim (Slack blocks, Gmail full headers, Telegram text_entities, Drive md5Checksum/webViewLink). Stored, not interpreted — so we never lose data and can promote a field to a typed slot later without re-ingesting.

9. How it's used

10. Open decisions (for the owner / next iteration)

  1. HTML email — convert to markdown (default) vs keep a raw html part in source_meta? (proposed: markdown body + raw in source_meta.)
  2. Media text-extraction default — off (metadata only) until the owner opts a source in. (proposed: off by default.)
  3. Entity-resolution aggressiveness — auto-merge on email/phone only (safe) vs also fuzzy display-name within a source. (proposed: strong-handles auto, name fuzzy = suggest-only.)
  4. Reactions/edits — store for chat sources, or drop as noise for v1? (proposed: store; cheap and useful for "voice".)

All documentation →