Skip to content

Commit d995615

Browse files
committed
fix(connectors): keep the newest messages when a chat transcript reaches the size limit
1 parent 9fc991a commit d995615

4 files changed

Lines changed: 133 additions & 38 deletions

File tree

apps/sim/connectors/google-chat/google-chat.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -328,24 +328,27 @@ function formatSpaceContent(
328328
space: Space,
329329
messages: ChatMessage[]
330330
): { content: string; messageCount: number } {
331-
const parts = new BoundedLines()
332-
parts.push(`Space: ${spaceTitle(space)}`)
331+
/** The newest messages survive when the window does not fit; the header always does. */
332+
const parts = new BoundedLines(CONNECTOR_TEXT_DOCUMENT_MAX_BYTES, 'last')
333+
parts.pin(`Space: ${spaceTitle(space)}`)
333334
const description = space.spaceDetails?.description?.trim()
334-
if (description) parts.push(`Description: ${description}`)
335+
if (description) parts.pin(`Description: ${description}`)
335336
const guidelines = space.spaceDetails?.guidelines?.trim()
336-
if (guidelines) parts.push(`Guidelines: ${guidelines}`)
337+
if (guidelines) parts.pin(`Guidelines: ${guidelines}`)
337338

338-
let messageCount = 0
339+
let headed = false
339340
for (const message of messages) {
340341
const text = message.text?.trim() || message.fallbackText?.trim()
341342
if (!text) continue
342-
if (messageCount === 0) parts.push('', '--- Messages ---')
343+
if (!headed) {
344+
parts.pin('', '--- Messages ---')
345+
headed = true
346+
}
343347
const timestamp = message.createTime ?? ''
344-
if (!parts.push(`[${timestamp}] ${senderLabel(message.sender)}: ${text}`)) break
345-
messageCount += 1
348+
parts.push(`[${timestamp}] ${senderLabel(message.sender)}: ${text}`)
346349
}
347350

348-
return { content: parts.join(), messageCount }
351+
return { content: parts.join(), messageCount: parts.count }
349352
}
350353

351354
export const googleChatConnector: ConnectorConfig = {

apps/sim/connectors/slack/slack.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -385,13 +385,17 @@ function walkBlockText(node: unknown, out: string[]): void {
385385
* Each entry: "[ISO timestamp] username: message text" (text may span lines
386386
* when the message has rich attachment/block content).
387387
*/
388+
/**
389+
* Appends the messages to the transcript oldest first. The transcript keeps
390+
* its newest messages when the window does not fit, so a message is skipped
391+
* only when it cannot fit on its own.
392+
*/
388393
async function appendMessages(
389394
accessToken: string,
390395
lines: BoundedLines,
391396
messages: SlackMessage[],
392397
syncContext?: Record<string, unknown>
393-
): Promise<number> {
394-
let appended = 0
398+
): Promise<void> {
395399
/** Slack returns newest first; the transcript reads oldest first. */
396400
const chronological = [...messages].reverse()
397401

@@ -412,11 +416,8 @@ async function appendMessages(
412416
? await resolveUserName(accessToken, msg.user, syncContext)
413417
: msg.username || 'unknown'
414418

415-
if (!lines.push(`[${timestamp}] ${userName}: ${content}`)) break
416-
appended += 1
419+
lines.push(`[${timestamp}] ${userName}: ${content}`)
417420
}
418-
419-
return appended
420421
}
421422

422423
/**
@@ -601,14 +602,15 @@ async function buildSlackChannelDocument(
601602
maxMessages
602603
)
603604

604-
const lines = new BoundedLines()
605-
lines.push(`Channel: #${channel.name}`)
605+
const lines = new BoundedLines(CONNECTOR_TEXT_DOCUMENT_MAX_BYTES, 'last')
606+
lines.pin(`Channel: #${channel.name}`)
606607
const topic = channel.topic?.value?.trim()
607-
if (topic) lines.push(`Topic: ${topic}`)
608+
if (topic) lines.pin(`Topic: ${topic}`)
608609
const purpose = channel.purpose?.value?.trim()
609-
if (purpose) lines.push(`Purpose: ${purpose}`)
610-
lines.push('')
611-
const messageCount = await appendMessages(accessToken, lines, messages, syncContext)
610+
if (purpose) lines.pin(`Purpose: ${purpose}`)
611+
lines.pin('')
612+
await appendMessages(accessToken, lines, messages, syncContext)
613+
const messageCount = lines.count
612614

613615
/**
614616
* Edit/thread fingerprint: max(edited.ts) and max(latest_reply) across the

apps/sim/connectors/utils.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1662,4 +1662,37 @@ describe('BoundedLines', () => {
16621662
expect(lines.push('éé')).toBe(true)
16631663
expect(lines.push('é')).toBe(false)
16641664
})
1665+
1666+
describe('keeping the last records', () => {
1667+
it('lets the oldest records go so the newest fit, under a header that stays', () => {
1668+
const lines = new BoundedLines(24, 'last')
1669+
lines.pin('# room')
1670+
expect(lines.push('one')).toBe(true)
1671+
expect(lines.push('two')).toBe(true)
1672+
expect(lines.push('three')).toBe(true)
1673+
expect(lines.push('four')).toBe(true)
1674+
expect(lines.count).toBe(3)
1675+
expect(lines.join()).toBe(
1676+
'# room\n[Truncated: earlier text was left out to fit the size limit]\ntwo\nthree\nfour'
1677+
)
1678+
})
1679+
1680+
it('refuses only a record that cannot fit on its own and carries on', () => {
1681+
const lines = new BoundedLines(12, 'last')
1682+
expect(lines.push('a very long record')).toBe(false)
1683+
expect(lines.push('short')).toBe(true)
1684+
expect(lines.push('next')).toBe(true)
1685+
expect(lines.count).toBe(2)
1686+
expect(lines.join()).toBe(
1687+
'[Truncated: earlier text was left out to fit the size limit]\nshort\nnext'
1688+
)
1689+
})
1690+
1691+
it('joins the header and records plainly when everything fits', () => {
1692+
const lines = new BoundedLines(64, 'last')
1693+
lines.pin('# room', '')
1694+
lines.push('hello')
1695+
expect(lines.join()).toBe('# room\n\nhello')
1696+
})
1697+
})
16651698
})

apps/sim/connectors/utils.ts

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -806,39 +806,96 @@ export function isSkippableMicrosoftGraphFolderError(
806806
*/
807807
export const CONNECTOR_TEXT_DOCUMENT_MAX_BYTES = 12 * 1024 * 1024
808808

809-
const TRUNCATION_NOTICE = '[Truncated: the indexed text reached the size limit]'
809+
const TRAILING_TRUNCATION_NOTICE = '[Truncated: the indexed text reached the size limit]'
810+
const LEADING_TRUNCATION_NOTICE = '[Truncated: earlier text was left out to fit the size limit]'
810811

811812
/**
812-
* Accumulates newline-joined text under a byte ceiling. A record is appended
813-
* whole or not at all, so a truncated document never ends mid-message, and the
814-
* output carries a notice when something was left out.
813+
* Which end of the stream survives when it does not fit: `first` keeps what
814+
* was pushed first and refuses the rest (a mail thread, whose root message is
815+
* the context), `last` keeps what was pushed last and lets older records go
816+
* (a chat transcript, whose newest messages are the ones people search for).
817+
*/
818+
export type BoundedLinesKeep = 'first' | 'last'
819+
820+
interface BoundedRecord {
821+
lines: string[]
822+
bytes: number
823+
}
824+
825+
function byteSize(lines: readonly string[]): number {
826+
let size = 0
827+
for (const line of lines) size += Buffer.byteLength(line, 'utf8') + 1
828+
return size
829+
}
830+
831+
/**
832+
* Accumulates newline-joined text under a byte ceiling. A record is kept
833+
* whole or not at all, so a truncated document never ends mid-message, and
834+
* the output carries a notice where something was left out.
815835
*/
816836
export class BoundedLines {
817-
private readonly lines: string[] = []
818-
private bytes = 0
837+
private readonly pinned: string[] = []
838+
private readonly records: BoundedRecord[] = []
839+
private pinnedBytes = 0
840+
private recordBytes = 0
819841
private truncated = false
820842

821-
constructor(private readonly maxBytes = CONNECTOR_TEXT_DOCUMENT_MAX_BYTES) {}
843+
constructor(
844+
private readonly maxBytes = CONNECTOR_TEXT_DOCUMENT_MAX_BYTES,
845+
private readonly keep: BoundedLinesKeep = 'first'
846+
) {}
847+
848+
/** Lines that open the document and are never let go, such as its header; counted against the ceiling. */
849+
pin(...lines: string[]): void {
850+
this.pinned.push(...lines)
851+
this.pinnedBytes += byteSize(lines)
852+
}
853+
854+
/** Records currently kept. */
855+
get count(): number {
856+
return this.records.length
857+
}
822858

823859
/**
824-
* Appends the lines together when they fit; otherwise marks the document
825-
* truncated, appends nothing, and returns false so the caller stops.
860+
* Appends the lines as one record and returns whether it was kept. Keeping
861+
* the first, a record that does not fit is refused, and so is every later
862+
* one, so a caller can stop. Keeping the last, a record is refused only when
863+
* it cannot fit on its own, and appending it lets the oldest records go
864+
* until the rest fits, so a caller carries on.
826865
*/
827866
push(...lines: string[]): boolean {
828-
if (this.truncated) return false
829-
let size = 0
830-
for (const line of lines) size += Buffer.byteLength(line, 'utf8') + 1
831-
if (this.bytes + size > this.maxBytes) {
867+
const bytes = byteSize(lines)
868+
if (this.keep === 'first') {
869+
if (this.truncated) return false
870+
if (this.pinnedBytes + this.recordBytes + bytes > this.maxBytes) {
871+
this.truncated = true
872+
return false
873+
}
874+
this.records.push({ lines, bytes })
875+
this.recordBytes += bytes
876+
return true
877+
}
878+
if (this.pinnedBytes + bytes > this.maxBytes) {
832879
this.truncated = true
833880
return false
834881
}
835-
this.lines.push(...lines)
836-
this.bytes += size
882+
this.records.push({ lines, bytes })
883+
this.recordBytes += bytes
884+
while (this.pinnedBytes + this.recordBytes > this.maxBytes) {
885+
const oldest = this.records.shift()
886+
if (!oldest) break
887+
this.recordBytes -= oldest.bytes
888+
this.truncated = true
889+
}
837890
return true
838891
}
839892

840-
/** Joins the accepted lines, ending with the truncation notice when a push was refused. */
893+
/** Joins the kept lines, with the truncation notice where records were left out. */
841894
join(): string {
842-
return this.truncated ? [...this.lines, TRUNCATION_NOTICE].join('\n') : this.lines.join('\n')
895+
const body = this.records.flatMap((record) => record.lines)
896+
if (!this.truncated) return [...this.pinned, ...body].join('\n')
897+
return this.keep === 'first'
898+
? [...this.pinned, ...body, TRAILING_TRUNCATION_NOTICE].join('\n')
899+
: [...this.pinned, LEADING_TRUNCATION_NOTICE, ...body].join('\n')
843900
}
844901
}

0 commit comments

Comments
 (0)