You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit bc27482
Browse filesBrowse the repository at this point in the historyBrowse files
fix(twister): require channelId on NewLinkWithNotes so write-back can't silently break
NewLinkWithNotes.channelId (the type saveLink()/saveLinks() accept) was
optional, which made it easy to set channelId only inside meta and
forget the top-level field. That compiled fine but silently broke
outbound write-back: the platform reads a link's channel back from the
persisted channelId column, not from meta, when resolving it for
connector callbacks like onNoteCreated. A link built that way would
send replies to nowhere with no error anywhere.
channelId is now required on NewLinkWithNotes, so this fails to compile
instead of failing silently in production. Added CreateLinkResult as
the return type for Connector.onCreateLink(), where channelId stays
optional (the platform auto-fills it from the compose draft when
omitted, since there's no equivalent auto-fill for saveLink()/saveLinks()
calls).
Fixed every connector in this repo affected by the tightened type,
several of which had exactly the bug this catches: channelId set only
in meta, never at the top level.
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Changed: `NewLinkWithNotes.channelId` (the type `integrations.saveLink()`/`saveLinks()` accept) is now required instead of optional. It was easy to set `channelId` only inside `meta` and forget the top-level field — that compiled fine but silently broke outbound write-back (`onNoteCreated` reads the channel back from the persisted `channelId`, not from `meta`) and bulk operations like `archiveLinks({ channelId })` on disable, with no error anywhere. The type system now catches this at compile time instead.
6
+
7
+
Added: `CreateLinkResult` — the return type for `Connector.onCreateLink()`. Identical to `NewLinkWithNotes` except `channelId` stays optional, since the platform auto-fills it from the compose draft when omitted. Connectors implementing `onCreateLink` should update their return type from `NewLinkWithNotes | null` to `CreateLinkResult | null`.
`channelId` is required on `NewLinkWithNotes` (the type `saveLink()`/`saveLinks()` accept) precisely because it's easy to set only inside `meta` and forget the top-level field — the type system will reject a link built that way. This isn't just about bulk operations like `integrations.archiveLinks({ channelId })` on disable: the platform persists `channelId` to the link's DB row and reads it back from there — not from `meta` — to populate `thread.meta.channelId` for connector callbacks like `onNoteCreated`. A link with `channelId` only inside `meta` will send replies to nobody: the connector's own `onNoteCreated` reads a channelId that was never actually saved, and typically no-ops silently (no error, nothing captured) because it can't resolve a client for the reply. Set `channelId` at the top level on every value you pass to `saveLink()`/`saveLinks()`.
312
+
313
+
`onCreateLink` is the one exception: its return type is `CreateLinkResult`, where `channelId` is optional — the platform auto-fills it from `draft.channelId` (the channel the user composed into) if you omit it.
314
+
311
315
## Classifier facets (optional)
312
316
313
317
Messaging-style connectors may set `link.facets` (`format` / `automation` / `reach` from `@plotday/twister/facets`) as internal classifier signal. Set a dimension only when a heuristic is confident; leave it `null`/omitted otherwise. See `gmail/src/gmail-facets.ts` and `slack/src/slack-facets.ts`.
@@ -520,7 +524,7 @@ For closed-roster DM-style compose set `compose.targets: "contacts"`; for open a
520
524
Then implement `onCreateLink` — return the link, do NOT call `integrations.saveLink()` (platform wires it to the originating thread):
2.Setting `channelId` only inside `link.meta` instead of at the top level → `NewLinkWithNotes.channelId` is what the platform actually persists and reads back for connector callbacks (`thread.meta.channelId` in `onNoteCreated`, etc.) and for bulk operations like `integrations.archiveLinks({ channelId })`on disable. A `meta`-only channelId compiles fine structurally but leaves outbound replies (and disable-time cleanup) silently broken — no error, nothing captured. The type system now requires the top-level field on `NewLinkWithNotes`, so this fails to compile instead of failing silently in production.
626
630
3. Not propagating `initialSync` through the whole pipeline → notification spam.
627
631
4. Mutable ids in `source` (e.g. Jira issue key) → use immutable id, store key in `meta`.
628
632
5.`source` that's only unique within one user's account → breaks cross-user dedup; add workspace/tenant/mailbox qualifier.
0 commit comments