Skip to content

Commit c40ece5

Browse files
alari76claude
andcommitted
fix: prevent model/session message cascade on OpenCode session start
- Skip restart in setModel when the model hasn't actually changed - Don't reset _lastReportedModel on process start (prevents duplicate model messages across restarts) - Only broadcast "Session started" for the first start, not for restarts triggered by model changes - Add debug SSE logging (behind CODEKIN_DEBUG_SSE env var) to diagnose thinking/text mixing with models like Kimi Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d8b053f commit c40ece5

3 files changed

Lines changed: 23 additions & 5 deletions

File tree

server/opencode-process.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,9 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
490490
if (!this.isOwnSession(properties)) break
491491
const field = properties.field as string | undefined
492492
const delta = properties.delta as string | undefined
493+
if (process.env.CODEKIN_DEBUG_SSE) {
494+
console.log(`[opencode-sse] delta field=${field} len=${delta?.length ?? 0} text=${delta?.slice(0, 80)}`)
495+
}
493496
if (field === 'text' && delta) {
494497
this.receivedDeltas = true
495498
// Buffer initial deltas to detect and strip user echo prefix.
@@ -539,6 +542,10 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
539542
// Only process events for our session
540543
if (!this.isOwnSession(properties)) break
541544

545+
if (process.env.CODEKIN_DEBUG_SSE) {
546+
console.log(`[opencode-sse] part.updated type=${part.type} len=${part.text?.length ?? 0} text=${part.text?.slice(0, 80)} receivedDeltas=${this.receivedDeltas} emittedPartText=${this.emittedPartText}`)
547+
}
548+
542549
switch (part.type) {
543550
case 'text': {
544551
// Text may arrive via message.part.delta (streaming) or as full
@@ -709,6 +716,12 @@ export class OpenCodeProcess extends EventEmitter<ClaudeProcessEvents> implement
709716
parts?: OpenCodeMessagePart[]
710717
} | undefined
711718
if (!info || info.role !== 'assistant' || !info.parts) break
719+
if (process.env.CODEKIN_DEBUG_SSE) {
720+
console.log(`[opencode-sse] message.updated parts=${info.parts.length} types=${info.parts.map(p => p.type).join(',')}`)
721+
for (const p of info.parts) {
722+
console.log(`[opencode-sse] part type=${p.type} text=${p.text?.slice(0, 120)}`)
723+
}
724+
}
712725
for (const part of info.parts) {
713726
this.handleSSEEvent({ type: 'message.part.updated', properties: { ...properties, part } })
714727
}

server/session-lifecycle.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,16 @@ export class SessionLifecycle {
172172

173173
cp.start()
174174
session.claudeProcess = cp
175-
// Reset so the next system_init always broadcasts the model message,
176-
// even if the model hasn't changed since the previous process.
177-
session._lastReportedModel = undefined
178175
this.deps.globalBroadcast?.({ type: 'sessions_updated' })
179176

177+
// Only show "Session started" for the initial start, not for restarts
178+
// triggered by model/permission changes — those already show a model message.
179+
const isRestart = !!session._lastReportedModel
180180
const startMsg: WsServerMessage = { type: 'claude_started', sessionId }
181181
this.deps.addToHistory(session, startMsg)
182-
this.deps.broadcast(session, startMsg)
182+
if (!isRestart) {
183+
this.deps.broadcast(session, startMsg)
184+
}
183185
return true
184186
}
185187

server/session-manager.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1211,7 +1211,10 @@ export class SessionManager {
12111211
setModel(sessionId: string, model: string): boolean {
12121212
const session = this.sessions.get(sessionId)
12131213
if (!session) return false
1214-
session.model = model || undefined
1214+
const newModel = model || undefined
1215+
// Skip restart if the model hasn't actually changed.
1216+
if (session.model === newModel) return true
1217+
session.model = newModel
12151218
// Clear any pending restart timer from a prior crash to prevent a stale
12161219
// timer from spawning a second process after we restart below.
12171220
if (session._restartTimer) { clearTimeout(session._restartTimer); session._restartTimer = undefined }

0 commit comments

Comments
 (0)