| 146 | * structural typing. |
| 147 | */ |
| 148 | export function createPiTranscript( |
| 149 | source: unknown[], |
| 150 | sessionId: string | undefined, |
| 151 | entryIds?: readonly (string | undefined)[], |
| 152 | ): Transcript & { |
| 153 | /** |
| 154 | * Pi-only escape hatch: returns the rebuilt message array suitable |
| 155 | * for `{ messages }` in the `pi.on("context", ...)` result. Returns |
| 156 | * the original array if no mutations occurred — preserves identity |
| 157 | * so Pi can short-circuit downstream cache invalidation. |
| 158 | */ |
| 159 | getOutputMessages(): unknown[]; |
| 160 | /** |
| 161 | * Pi-only escape hatch: the mutable `working` array that part proxies |
| 162 | * (tagging, drops, caveman) write to and `commit()` flushes back to source. |
| 163 | * |
| 164 | * Phases that mutate messages OUTSIDE the transcript part API — reasoning |
| 165 | * clearing/replay, which set `part.thinking = "[cleared]"` in place — MUST |
| 166 | * target this array, not the original `source`. Tagging/drops/caveman |
| 167 | * REASSIGN `working[idx]` to fresh spread-copied objects; if reasoning mutated |
| 168 | * `source[idx]` (a now-divergent object) instead, the later `commit()` would |
| 169 | * overwrite `source[idx] = working[idx]` and silently discard the reasoning |
| 170 | * mutation while the cleared-reasoning watermark still advanced — a defer-pass |
| 171 | * replay divergence (wire keeps original reasoning, state says cleared) that |
| 172 | * busts the prompt cache. Routing reasoning through `working` keeps every |
| 173 | * mutation in the single channel `commit()` flushes. |
| 174 | */ |
| 175 | getWorkingMessages(): PiAgentMessage[]; |
| 176 | } { |
| 177 | const working = source.slice() as unknown as PiAgentMessage[]; |
| 178 | const dirtyMessages = new Set<number>(); |
| 179 | |
| 180 | // Normalize: fold consecutive toolResult runs into the immediately |
| 181 | // following user message as tool_result transcript parts. Track |
| 182 | // source-array locations so commit() can write mutations back. |
| 183 | const transcriptMessages: TranscriptMessage[] = buildTranscriptView( |
| 184 | working, |
| 185 | sessionId, |
| 186 | (messageIndex) => { |
| 187 | dirtyMessages.add(messageIndex); |
| 188 | }, |
| 189 | entryIds, |
| 190 | ); |
| 191 | |
| 192 | let committed = false; |
| 193 | |
| 194 | return { |
| 195 | messages: transcriptMessages, |
| 196 | harness: "pi", |
| 197 | commit(): void { |
| 198 | if (committed) return; |
| 199 | committed = true; |
| 200 | // Sync mutations from `working` back into `source` so that |
| 201 | // any structural changes the caller applies to `source` |
| 202 | // directly (e.g. `<session-history>` injection's splice + |
| 203 | // message[0] prepend) compose correctly with our part-level |
| 204 | // mutations. |
| 205 | // |