|
|
@@ -349,6 +349,40 @@ export class ChatTranscriptStore {
|
|
|
.get(...scope)!.n,
|
|
|
)
|
|
|
}
|
|
|
+ private parkOpenAnswer(scope: Scope, state: TurnHead) {
|
|
|
+ if (!state.current) return
|
|
|
+ const active = this.message(...scope, state.current)
|
|
|
+ const visible = Boolean(
|
|
|
+ active &&
|
|
|
+ (String(active.content ?? '').trim() ||
|
|
|
+ String(active.reasoning ?? '').trim() ||
|
|
|
+ (Array.isArray(active.tool_calls) && active.tool_calls.length)),
|
|
|
+ )
|
|
|
+ if (active && visible) this.put(scope, { ...active, status: 'complete', final_result: false })
|
|
|
+ else if (active) {
|
|
|
+ const revision = active.revision + 1
|
|
|
+ this.db
|
|
|
+ .prepare(
|
|
|
+ 'UPDATE chat_transcript_messages SET deleted=1,revision=? WHERE owner=? AND profile=? AND session_id=? AND id=?',
|
|
|
+ )
|
|
|
+ .run(revision, ...scope, active.id)
|
|
|
+ this.event(...scope, 'message.deleted', { id: active.id, revision })
|
|
|
+ }
|
|
|
+ state.current = undefined
|
|
|
+ state.preview = false
|
|
|
+ }
|
|
|
+ /** A later completion often repeats the whole turn. Keep only the part written after this row. */
|
|
|
+ private placedOutput(scope: Scope, row: TranscriptMessage | undefined, turn: string | undefined, output: unknown, fallback: unknown): unknown {
|
|
|
+ if (typeof output !== 'string') return output ?? fallback
|
|
|
+ if (!output || !turn) return output || fallback
|
|
|
+ const prefix = this.all(...scope)
|
|
|
+ .filter((message) => message.role === 'assistant' && message.turn_id === turn && message.id !== row?.id && (!row || message.seq < row.seq))
|
|
|
+ .sort((left, right) => left.seq - right.seq)
|
|
|
+ .map((message) => String(message.content ?? ''))
|
|
|
+ .join('')
|
|
|
+ if (!prefix || !output.startsWith(prefix)) return output
|
|
|
+ return output.slice(prefix.length) || fallback
|
|
|
+ }
|
|
|
private put(scope: Scope, value: Data): TranscriptMessage {
|
|
|
const position = Number(
|
|
|
this.db
|
|
|
@@ -694,6 +728,10 @@ export class ChatTranscriptStore {
|
|
|
if (!this.alias(...scope, `client:${client}`)) {
|
|
|
const previousRunning = state.running,
|
|
|
steer = p.method === 'session.steer' && state.running
|
|
|
+ // The open answer already has a sequence. Parking it here lets the
|
|
|
+ // inserted user row land at this moment, and the rest of the reply
|
|
|
+ // continues in a later row.
|
|
|
+ if (steer) this.parkOpenAnswer(scope, state)
|
|
|
const turn = steer ? state.turn! : randomUUID()
|
|
|
const user = this.put(scope, {
|
|
|
id: `user:${delivery}`,
|
|
|
@@ -899,9 +937,11 @@ export class ChatTranscriptStore {
|
|
|
type === 'message.complete' && !failed && !interrupted && !explicitStop && (toolRound || openTools)
|
|
|
const output = p.output ?? p.text ?? p.content ?? p.message?.content
|
|
|
if (continues) {
|
|
|
- if (old || output || p.attachments)
|
|
|
+ const placed = this.placedOutput(scope, old, state.turn, output, old?.content ?? '')
|
|
|
+ const hasPlaced = typeof placed === 'string' ? placed.length > 0 : Boolean(placed)
|
|
|
+ if (old || hasPlaced || p.attachments)
|
|
|
saveAssistant({
|
|
|
- content: output === '' ? (old?.content ?? '') : (output ?? old?.content ?? ''),
|
|
|
+ content: placed,
|
|
|
status: 'streaming',
|
|
|
final_result: false,
|
|
|
})
|
|
|
@@ -910,9 +950,11 @@ export class ChatTranscriptStore {
|
|
|
state.queued = state.queue.length > 0 || Number(p.queue_remaining ?? 0) > 0
|
|
|
} else {
|
|
|
state.terminal = false
|
|
|
- if (old || output || p.attachments)
|
|
|
+ const placed = this.placedOutput(scope, old, state.turn, output, old?.content ?? '')
|
|
|
+ const hasPlaced = typeof placed === 'string' ? placed.length > 0 : Boolean(placed)
|
|
|
+ if (old || hasPlaced || p.attachments)
|
|
|
saveAssistant({
|
|
|
- content: output === '' ? (old?.content ?? '') : (output ?? old?.content ?? ''),
|
|
|
+ content: placed,
|
|
|
status: failed ? 'failed' : interrupted ? 'interrupted' : 'complete',
|
|
|
segment_kind: 'answer',
|
|
|
final_result: !failed && !interrupted,
|