Skip to content

Commit 10b3623

Browse files
dmitriyzhukclaude
andcommitted
feat(bridle): "New chat" archives the transcript instead of clearing locally (v0.13.3)
Closes the loop on the New chat UX. Before: local chat cleared but the server-side transcript was untouched, and the next reload would restore it (token-flow) — fixed in v0.13.2 by suppressing the next replay, but the history just kept piling up on the server with no way to roll it over. Now the SDK calls POST /api/agent/<id>/transcript/archive?channel=<...> right before the reconnect. Two implementations, hub-side: 1. Bridle hub (bridle/nestjs/): new IBridleTranscriptGateway.archive() with a default that falls back to delete(). Integrators override to do the "right thing" for their storage (rename to timestamped sibling, move to cold tier, etc.). Controller endpoint POST :agentId/transcript/archive returns { archivedPath? }. 2. Ranch hub (ranch/api/src/slices/bridle/): the controller reads data/sessions/bridle:<channel>.jsonl, writes the same content to data/sessions/bridle:<channel>.<iso-ts>.archived.jsonl, then deletes the live file. No-op when the live file is empty/missing. Admins still have full history under the .archived. files; the visitor gets a clean slate. SDK side: archive call is best-effort. If the endpoint isn't there (older hub) or the request fails, we still clear locally and reconnect — the existing skipNextTranscript flag keeps the replay from undoing the clear. Net effect: New chat is consistent regardless of which hub version the embed is talking to. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0b04392 commit 10b3623

4 files changed

Lines changed: 66 additions & 6 deletions

File tree

nestjs/bridle.controller.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,26 @@ export class BridleController {
137137
this.logger.warn(`Transcript reset failed for ${agentId}/${channel}: ${(err as Error).message}`)
138138
}
139139
}
140+
141+
@ApiOperation({
142+
description:
143+
'Archive the persisted chat transcript for an agent/channel. Used by the embed\'s "New chat" action when the integrator wants the visitor to see a clean slate but still keep the prior conversation for admin/audit. Default integrator binding falls back to delete; override IBridleTranscriptGateway.archive() to move-with-timestamp instead.',
144+
operationId: 'archiveBridleTranscript',
145+
})
146+
@ApiQuery({ name: 'channel', required: false, description: 'Session channel — defaults to "admin".' })
147+
@FlatResponse()
148+
@Post(':agentId/transcript/archive')
149+
@HttpCode(200)
150+
async archiveTranscript(
151+
@Param('agentId') agentId: string,
152+
@Query('channel') channelRaw?: string,
153+
): Promise<{ archivedPath?: string }> {
154+
const channel = (channelRaw ?? 'admin').trim() || 'admin'
155+
try {
156+
return await this.transcripts.archive(agentId, channel)
157+
} catch (err) {
158+
this.logger.warn(`Transcript archive failed for ${agentId}/${channel}: ${(err as Error).message}`)
159+
return {}
160+
}
161+
}
140162
}

nestjs/domain/bridleTranscript.gateway.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,22 @@ export abstract class IBridleTranscriptGateway {
3737
* succeed when nothing exists.
3838
*/
3939
abstract delete(agentId: string, channel: string): Promise<void>
40+
41+
/**
42+
* Move the transcript for `(agentId, channel)` aside so subsequent
43+
* `read()` calls return an empty list, without losing the data — used
44+
* by the embed's "New chat" action when the visitor wants a fresh
45+
* conversation but the integrator wants to keep the history for
46+
* admin/audit. Implementations typically rename the live file with
47+
* a timestamp suffix (e.g. `bridle:<channel>.<iso-ts>.archived.jsonl`)
48+
* and return that path. Returning `{}` is fine when nothing was
49+
* there to archive in the first place.
50+
*
51+
* Default implementation falls back to `delete()` — integrators can
52+
* upgrade by overriding this method.
53+
*/
54+
async archive(agentId: string, channel: string): Promise<{ archivedPath?: string }> {
55+
await this.delete(agentId, channel)
56+
return {}
57+
}
4058
}

sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@cleanslice/bridle",
3-
"version": "0.13.2",
3+
"version": "0.13.3",
44
"description": "Embeddable web chat for Bridle — drop-in <script> or programmatic init.",
55
"type": "module",
66
"main": "./dist/bridle.mjs",

sdk/src/BridleChat.ce.vue

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,28 @@ function toggleMenu(): void {
787787
*/
788788
async function startNewChat(): Promise<void> {
789789
menuOpen.value = false
790+
// Ask the hub to archive the current transcript before we reconnect —
791+
// best-effort: if the endpoint isn't there (older hub) or the request
792+
// fails, we still clear locally so the visitor's UI is consistent.
793+
// Server-side hubs that don't override IBridleTranscriptGateway.archive
794+
// fall back to delete(); Ranch's controller moves the live JSONL to a
795+
// timestamped sibling so admins still have the conversation.
796+
const channel = client?.getClientId?.()
797+
if (channel && props.apiUrl && props.agentId) {
798+
try {
799+
const url =
800+
`${props.apiUrl.replace(/\/$/, '')}` +
801+
`/api/agent/${encodeURIComponent(props.agentId)}/transcript/archive` +
802+
`?channel=${encodeURIComponent(channel)}`
803+
const headers: Record<string, string> = {}
804+
if (typeof props.token === 'string' && props.token) {
805+
headers.Authorization = `Bearer ${props.token}`
806+
}
807+
await fetch(url, { method: 'POST', headers })
808+
} catch (err) {
809+
console.warn('[bridle] transcript archive failed (continuing):', err)
810+
}
811+
}
790812
if (typeof window !== 'undefined') {
791813
try {
792814
window.localStorage.removeItem(`bridle:anon:${props.agentId}`)
@@ -801,11 +823,9 @@ async function startNewChat(): Promise<void> {
801823
greetingShown.value = false
802824
isTyping.value = false
803825
connectionError.value = null
804-
// Tell the next welcome handler to skip its transcript replay. Server-
805-
// side history isn't purged (no hub endpoint for that), but suppressing
806-
// the replay is what the user actually wanted from "New chat" — a
807-
// visibly fresh conversation. Continuity on subsequent reloads is
808-
// preserved unless they hit New chat again.
826+
// Suppress the next transcript replay too — belt-and-braces in case the
827+
// archive endpoint was a no-op (older hub without the override) and the
828+
// live transcript wasn't actually moved.
809829
skipNextTranscript = true
810830
if (props.apiUrl && props.agentId) {
811831
await connect()

0 commit comments

Comments
 (0)