fence-attrs.tsfix(coaching): stop the callout round-trip and the warm-up gate losing real data
Review findings on this branch. Four of them lose or suppress real content:
- Serializing a callout with plainText dropped every inline mark inside it, so
bold, links and widgets vanished on each save of the markdown mirror. Use
renderInline, and collapse breaks to a space since the marker syntax is
single-line on both sides.
- Converting a blockquote to a callout lifted its first paragraph and discarded
every sibling block. A callout is inline-only, so anything larger now stays a
blockquote rather than losing content.
- isWarmupSeedThread fired on ANY message carrying a seed code. Warm-up mail
gets merged into live deal threads, and one stray message would then suppress
the agent on a real deal on every subsequent sync, silently and forever.
Require every non-draft message to carry a code.
- The moment card's deal crumb pointed at /conversations/:id, which stopped
being a route; it opens at /agent?conversationId= via a router Link.
And the smaller ones: decodeURIComponent throws on a malformed attribute and
would take down the editor from inside parseHTML, the recording link is
agent-authored so it needs a scheme check, --days reached Postgres as
'NaN days'::interval, a code inside a table cell was missed, and the radar
dropped a series whose header matched the score column.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 25, 2026, 3:55 PMmoment-fence.tsfeat(documents): play the call inside the document, at the moment being discussed
A coaching note the rep cannot check against the tape is an assertion, and until now
checking it meant leaving the document for a provider page that opened at 0:00. A
```moment card now embeds the call positioned at `at:`, every timestamp in its
transcript is a button that seeks the player to that line, and a new ```recording
fence embeds a whole call anywhere in a document.
Playback resolves from `externalId`, never a stored URL. That is not a preference:
Circleback's `recording_url` is a signed link that expired 24h after ingest, so every
Circleback moment's "mandatory recording link" was already a 400, and Fathom's is a
web page rather than media. The id is stable, and the server mints from it per view.
Cedar's own <video> rather than a provider embed, because only an element we own can
be seeked — verified against the live providers rather than assumed. Fathom's
/embed/<token> IS frameable (no X-Frame-Options, no frame-ancestors, unlike /share),
but its component declares only {call, autoplay, shareUrl, displayMode} and nine
probed parameter names changed nothing, so it cannot be positioned; it is kept as the
last rung of the fallback ladder. Fathom's share page does deep-link — ?timestamp=125
comes back as currentTime 125.0 — so links out carry the moment. Gong's iframe takes
exact from/to but demands the viewer be signed into Gong, in an iframe, not in Safari.
Gong gains a media path it never had: /v2/calls/extensive with exposedFields.media,
and the tenant's real call URL read off metaData.url instead of the hardcoded
us-4796 subdomain. Its 403 returns `permission_required`, deliberately not
`unavailable` — the recording exists and Cedar may not fetch it, which is a problem
with a named owner and a one-minute fix, so the player names the
api:calls:read:media-url scope and where a Gong admin grants it. The connection form
now asks for it up front. This lives in gong-recording.ts, not gong.ts, because
gong.ts reaches crm/meeting-events -> meetings/index.ts -> back to gong.ts, and
importing the minter from there crashes on load with a TDZ error.
Nothing mints until the reader presses play: Fathom allows 30 download requests per
60s per key and renders a cold recording in ~34s, so a document with ten moments that
minted eagerly would spend a third of that budget before anyone pressed anything.
Not verified, and called out in the design doc: no live mint ran (this machine's
ORG_CREDENTIALS_ENCRYPTION_KEY does not match those connection rows), the Gong path
has never run against Gong since no org has a connection, and whether a seek lands on
the right words is still unproven — Fathom's call started_at and recording.started_at
differ by 62s on the one sample inspected.
Also carries a pre-existing working-tree change to fathom.ts that could not be
separated from the same file: transcript turns keep their timestamp, so the coaching
rubric's discovery-length row is scoreable for webhook-ingested calls. Its test is
included rather than left orphaned.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 3:02 PMMomentFenceNode.tsxfeat(documents): play the call inside the document, at the moment being discussed
A coaching note the rep cannot check against the tape is an assertion, and until now
checking it meant leaving the document for a provider page that opened at 0:00. A
```moment card now embeds the call positioned at `at:`, every timestamp in its
transcript is a button that seeks the player to that line, and a new ```recording
fence embeds a whole call anywhere in a document.
Playback resolves from `externalId`, never a stored URL. That is not a preference:
Circleback's `recording_url` is a signed link that expired 24h after ingest, so every
Circleback moment's "mandatory recording link" was already a 400, and Fathom's is a
web page rather than media. The id is stable, and the server mints from it per view.
Cedar's own <video> rather than a provider embed, because only an element we own can
be seeked — verified against the live providers rather than assumed. Fathom's
/embed/<token> IS frameable (no X-Frame-Options, no frame-ancestors, unlike /share),
but its component declares only {call, autoplay, shareUrl, displayMode} and nine
probed parameter names changed nothing, so it cannot be positioned; it is kept as the
last rung of the fallback ladder. Fathom's share page does deep-link — ?timestamp=125
comes back as currentTime 125.0 — so links out carry the moment. Gong's iframe takes
exact from/to but demands the viewer be signed into Gong, in an iframe, not in Safari.
Gong gains a media path it never had: /v2/calls/extensive with exposedFields.media,
and the tenant's real call URL read off metaData.url instead of the hardcoded
us-4796 subdomain. Its 403 returns `permission_required`, deliberately not
`unavailable` — the recording exists and Cedar may not fetch it, which is a problem
with a named owner and a one-minute fix, so the player names the
api:calls:read:media-url scope and where a Gong admin grants it. The connection form
now asks for it up front. This lives in gong-recording.ts, not gong.ts, because
gong.ts reaches crm/meeting-events -> meetings/index.ts -> back to gong.ts, and
importing the minter from there crashes on load with a TDZ error.
Nothing mints until the reader presses play: Fathom allows 30 download requests per
60s per key and renders a cold recording in ~34s, so a document with ten moments that
minted eagerly would spend a third of that budget before anyone pressed anything.
Not verified, and called out in the design doc: no live mint ran (this machine's
ORG_CREDENTIALS_ENCRYPTION_KEY does not match those connection rows), the Gong path
has never run against Gong since no org has a connection, and whether a seek lands on
the right words is still unproven — Fathom's call started_at and recording.started_at
differ by 62s on the one sample inspected.
Also carries a pre-existing working-tree change to fathom.ts that could not be
separated from the same file: transcript turns keep their timestamp, so the coaching
rubric's discovery-length row is scoreable for webhook-ingested calls. Its test is
included rather than left orphaned.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 29, 2026, 3:02 PMscorecard-fence.tsfix(coaching): stop the callout round-trip and the warm-up gate losing real data
Review findings on this branch. Four of them lose or suppress real content:
- Serializing a callout with plainText dropped every inline mark inside it, so
bold, links and widgets vanished on each save of the markdown mirror. Use
renderInline, and collapse breaks to a space since the marker syntax is
single-line on both sides.
- Converting a blockquote to a callout lifted its first paragraph and discarded
every sibling block. A callout is inline-only, so anything larger now stays a
blockquote rather than losing content.
- isWarmupSeedThread fired on ANY message carrying a seed code. Warm-up mail
gets merged into live deal threads, and one stray message would then suppress
the agent on a real deal on every subsequent sync, silently and forever.
Require every non-draft message to carry a code.
- The moment card's deal crumb pointed at /conversations/:id, which stopped
being a route; it opens at /agent?conversationId= via a router Link.
And the smaller ones: decodeURIComponent throws on a malformed attribute and
would take down the editor from inside parseHTML, the recording link is
agent-authored so it needs a scheme check, --days reached Postgres as
'NaN days'::interval, a code inside a table cell was missed, and the radar
dropped a series whose header matched the score column.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 25, 2026, 3:55 PMScorecardFenceNode.tsxfix(coaching): keep the dry-run CLI a thin client, and use absolute imports
The no-heavy-imports guard caught coaching-dryrun importing services/*.
convention-paths drags in documents/index — the DB handle, drizzle and env —
which is exactly the tsx/ESM boot weight the CLI-over-HTTP migration removed.
repSlugFromEmail was a pure three-line helper that merely happened to live in
that heavy module, and nothing inside it used the function. Split it into its
own module so the CLI shares the real implementation instead of reimplementing
it and drifting. seed-content imports nothing at all, so both it and rep-slug
are allow-listed as pure, with the reasoning recorded next to the exceptions.
Also switches the coaching fence node views to absolute imports, per the
frontend convention.
Includes markdown-prose-classes.ts from concurrent work in the tree.
Co-Authored-By: Claude Opus 5 (1M context) <<email>>Aug 25, 2026, 6:10 PM