(messages: Message[], ignore: boolean = false)
| 17 | * @param ignore When true, messages will not be recorded to the transcript |
| 18 | */ |
| 19 | export function useLogMessages(messages: Message[], ignore: boolean = false) { |
| 20 | const teamContext = useAppState(s => s.teamContext) |
| 21 | |
| 22 | // messages is append-only between compactions, so track where we left off |
| 23 | // and only pass the new tail to recordTranscript. Avoids O(n) filter+scan |
| 24 | // on every setMessages (~20x/turn, so n=3000 was ~120k wasted iterations). |
| 25 | const lastRecordedLengthRef = useRef(0) |
| 26 | const lastParentUuidRef = useRef<UUID | undefined>(undefined) |
| 27 | // First-uuid change = compaction or /clear rebuilt the array; length alone |
| 28 | // can't detect this since post-compact [CB,summary,...keep,new] may be longer. |
| 29 | const firstMessageUuidRef = useRef<UUID | undefined>(undefined) |
| 30 | // Guard against stale async .then() overwriting a fresher sync update when |
| 31 | // an incremental render fires before the compaction .then() resolves. |
| 32 | const callSeqRef = useRef(0) |
| 33 | |
| 34 | useEffect(() => { |
| 35 | if (ignore) return |
| 36 | |
| 37 | const currentFirstUuid = messages[0]?.uuid as UUID | undefined |
| 38 | const prevLength = lastRecordedLengthRef.current |
| 39 | |
| 40 | // First-render: firstMessageUuidRef is undefined. Compaction: first uuid changes. |
| 41 | // Both are !isIncremental, but first-render sync-walk is safe (no messagesToKeep). |
| 42 | const wasFirstRender = firstMessageUuidRef.current === undefined |
| 43 | const isIncremental = |
| 44 | currentFirstUuid !== undefined && |
| 45 | !wasFirstRender && |
| 46 | currentFirstUuid === firstMessageUuidRef.current && |
| 47 | prevLength <= messages.length |
| 48 | // Same-head shrink: tombstone filter, rewind, snip, partial-compact. |
| 49 | // Distinguished from compaction (first uuid changes) because the tail |
| 50 | // is either an existing on-disk message or a fresh message that this |
| 51 | // same effect's recordTranscript(fullArray) will write — see sync-walk |
| 52 | // guard below. |
| 53 | const isSameHeadShrink = |
| 54 | currentFirstUuid !== undefined && |
| 55 | !wasFirstRender && |
| 56 | currentFirstUuid === firstMessageUuidRef.current && |
| 57 | prevLength > messages.length |
| 58 | |
| 59 | const startIndex = isIncremental ? prevLength : 0 |
| 60 | if (startIndex === messages.length) return |
| 61 | |
| 62 | // Full array on first call + after compaction: recordTranscript's own |
| 63 | // O(n) dedup loop handles messagesToKeep interleaving correctly there. |
| 64 | const slice = startIndex === 0 ? messages : messages.slice(startIndex) |
| 65 | const parentHint = isIncremental ? lastParentUuidRef.current : undefined |
| 66 | |
| 67 | // Fire and forget - we don't want to block the UI. |
| 68 | const seq = ++callSeqRef.current |
| 69 | void recordTranscript( |
| 70 | slice, |
| 71 | isAgentSwarmsEnabled() |
| 72 | ? { |
| 73 | teamName: teamContext?.teamName, |
| 74 | agentName: teamContext?.selfAgentName, |
| 75 | } |
| 76 | : {}, |
no test coverage detected