my harness for niri
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

more deepseek specific fixes i hate them

+215 -10
-2
src/runner/index.ts
··· 245 245 }) 246 246 if (exit === "rest") { 247 247 setRunnerPresence("resting") 248 - } else if (exit === "silent_complete") { 249 - console.log("[runner] assistant ended turn with no tool call; stopping wake cycle") 250 248 } else { 251 249 console.warn("[runner] loop guard stopped the run; waiting for a new external event") 252 250 }
+50
src/runner/loop-completion.test.ts
··· 1 + import assert from "node:assert/strict" 2 + import test from "node:test" 3 + import { __completionTest } from "./loop-completion.js" 4 + 5 + test("consumeCompletionStream preserves reasoning_content on assistant messages", async () => { 6 + async function* stream() { 7 + yield { 8 + id: "chunk_1", 9 + object: "chat.completion.chunk", 10 + created: 0, 11 + model: "deepseek-v4-flash", 12 + choices: [ 13 + { 14 + index: 0, 15 + delta: { 16 + reasoning_content: "thinking...", 17 + }, 18 + finish_reason: null, 19 + }, 20 + ], 21 + } 22 + yield { 23 + id: "chunk_2", 24 + object: "chat.completion.chunk", 25 + created: 0, 26 + model: "deepseek-v4-flash", 27 + choices: [ 28 + { 29 + index: 0, 30 + delta: { 31 + content: "hello", 32 + }, 33 + finish_reason: "stop", 34 + }, 35 + ], 36 + usage: { 37 + prompt_tokens: 10, 38 + completion_tokens: 5, 39 + total_tokens: 15, 40 + }, 41 + } 42 + } 43 + 44 + const result = await __completionTest.consumeCompletionStream(stream() as never) 45 + const message = result.message as typeof result.message & { reasoning_content?: string } 46 + 47 + assert.equal(message.content, "hello") 48 + assert.equal(message.reasoning_content, "thinking...") 49 + assert.equal(result.bufferedThinking, "thinking...") 50 + })
+9
src/runner/loop-completion.ts
··· 382 382 : {}), 383 383 } 384 384 385 + if (reasoningParts.length > 0) { 386 + ;(message as OpenAI.Chat.ChatCompletionMessage & { reasoning_content?: string }).reasoning_content = 387 + reasoningParts.join("") 388 + } 389 + 385 390 return { 386 391 message, 387 392 usage, ··· 682 687 } 683 688 } 684 689 } 690 + 691 + export const __completionTest = { 692 + consumeCompletionStream, 693 + }
+51
src/runner/loop.test.ts
··· 35 35 assert.match(String(state.conversation[0]?.content), /did not call discord_send/i) 36 36 }) 37 37 38 + test("applyDiscordSendNudge fires after a harness restart when the discord event is pre-turn context", () => { 39 + const state = makeState() 40 + state.conversation.push( 41 + { 42 + role: "user", 43 + content: "[harness restarted — discord @ 2026-05-01T04:30:00.000Z]\n\n[discord/dm] hi starfish", 44 + }, 45 + { 46 + role: "assistant", 47 + content: "i keep getting bounced by harness restarts sorry ^^ still here though! what's up?", 48 + }, 49 + ) 50 + 51 + const turnStart = 1 52 + const turnMessages = [state.conversation[1]!] 53 + 54 + const nudged = __loopTest.applyDiscordSendNudge(state, turnMessages, turnStart) 55 + 56 + assert.equal(nudged, true) 57 + assert.equal(state.conversation.length, 3) 58 + assert.match(String(state.conversation[2]?.content), /did not call discord_send/i) 59 + }) 60 + 38 61 test("applyDiscordSendNudge does not fire when discord_send was already called", () => { 39 62 const state = makeState() 40 63 const turnMessages = [ ··· 63 86 assert.equal(nudged, false) 64 87 assert.equal(state.conversation.length, 0) 65 88 }) 89 + 90 + test("waitForNextEvent waits for and injects the next external event", async () => { 91 + const calls: string[] = [] 92 + const event = { 93 + source: "chat" as const, 94 + triggeredAt: "2026-05-01T04:40:00.000Z", 95 + content: "still here", 96 + raw: {}, 97 + } 98 + 99 + await __loopTest.waitForNextEvent(42, { 100 + waitForEvent: async () => { 101 + calls.push("wait") 102 + return event 103 + }, 104 + waitForEventWithTimeout: async () => null, 105 + injectIncomingEvent: (_convId, incoming) => { 106 + calls.push(`inject:${incoming.content}`) 107 + assert.equal(_convId, 42) 108 + assert.equal(incoming, event) 109 + }, 110 + flushDeferredEvents: () => {}, 111 + clearSession: async () => {}, 112 + saveSession: async () => {}, 113 + }) 114 + 115 + assert.deepEqual(calls, ["wait", "inject:still here"]) 116 + })
+44 -8
src/runner/loop.ts
··· 33 33 Rest = "rest", 34 34 } 35 35 36 - export type RunLoopExit = "rest" | "guard_stop" | "silent_complete" 36 + export type RunLoopExit = "rest" | "guard_stop" 37 + 38 + async function waitForNextEvent(convId: number, hooks: LoopHooks): Promise<void> { 39 + const incoming = await hooks.waitForEvent() 40 + hooks.injectIncomingEvent(convId, incoming) 41 + } 37 42 38 43 async function stopLoopForGuard(state: LoopState, hooks: LoopHooks, reason: string): Promise<RunLoopExit> { 39 44 const guardMessage = `[system] safety stop: ${reason}. pausing until a new external event wakes niri again.` ··· 76 81 * conversational text but did not call discord_send. Injects a system 77 82 * nudge so the next turn actually delivers the message. 78 83 */ 79 - function applyDiscordSendNudge(state: LoopState, turnMessages: OpenAI.Chat.ChatCompletionMessage[]): boolean { 80 - // Check if any incoming user message in this turn came from Discord 81 - const hasDiscordInput = turnMessages.some( 82 - (m) => m.role === "user" && typeof m.content === "string" && /\[discord\/(?:dm|batch|channel)\]/i.test(m.content), 83 - ) 84 + function isDiscordInputMessage(message: OpenAI.Chat.ChatCompletionMessage | OpenAI.Chat.ChatCompletionMessageParam): boolean { 85 + return message.role === "user" && typeof message.content === "string" && /\[discord\/(?:dm|batch|channel)\]/i.test(message.content) 86 + } 87 + 88 + function hasDiscordInputForTurn( 89 + conversation: OpenAI.Chat.ChatCompletionMessageParam[], 90 + turnMessages: OpenAI.Chat.ChatCompletionMessage[], 91 + turnStart: number, 92 + ): boolean { 93 + if (turnMessages.some(isDiscordInputMessage)) return true 94 + 95 + // After a harness restart, the triggering Discord event is appended before 96 + // the first post-restart assistant turn. Look backward to the latest 97 + // assistant boundary and treat intervening user messages as active context. 98 + for (let i = turnStart - 1; i >= 0; i--) { 99 + const message = conversation[i] 100 + if (!message) continue 101 + if (message.role === "assistant") break 102 + if (isDiscordInputMessage(message)) return true 103 + } 104 + 105 + return false 106 + } 107 + 108 + function applyDiscordSendNudge( 109 + state: LoopState, 110 + turnMessages: OpenAI.Chat.ChatCompletionMessage[], 111 + turnStart: number, 112 + ): boolean { 113 + // Check if the assistant is responding to active Discord input, including 114 + // the post-restart case where the triggering user message is already in the 115 + // conversation before the turn begins. 116 + const hasDiscordInput = hasDiscordInputForTurn(state.conversation, turnMessages, turnStart) 84 117 if (!hasDiscordInput) return false 85 118 86 119 // Check if the assistant called discord_send in this turn ··· 198 231 // then calls wait/rest, leaving the Discord user in silence. 199 232 let discordSendNudged = false 200 233 if (outcome !== CycleOutcome.Rest) { 201 - discordSendNudged = applyDiscordSendNudge(state, turnMessages) 234 + discordSendNudged = applyDiscordSendNudge(state, turnMessages, turnStart) 202 235 } 203 236 204 237 if (interruptedByUserEvent || !turnSignature) { ··· 215 248 if (outcome === CycleOutcome.NoTools) { 216 249 if (discordSendNudged) continue 217 250 await hooks.saveSession() 218 - return "silent_complete" 251 + await waitForNextEvent(convId, hooks) 252 + continue 219 253 } 220 254 221 255 if (turnCount >= RUNNER_MAX_TURNS) { ··· 238 272 239 273 export const __loopTest = { 240 274 applyDiscordSendNudge, 275 + hasDiscordInputForTurn, 276 + waitForNextEvent, 241 277 }
+23
src/runner/util.test.ts
··· 1 + import assert from "node:assert/strict" 2 + import test from "node:test" 3 + import { sanitizeMessages } from "./util.js" 4 + 5 + test("sanitizeMessages backfills empty reasoning_content for assistant history", () => { 6 + const messages = sanitizeMessages([ 7 + { 8 + role: "assistant", 9 + content: "plain reply", 10 + refusal: null, 11 + }, 12 + { 13 + role: "assistant", 14 + content: "reply with reasoning", 15 + refusal: null, 16 + reasoning_content: "thinking...", 17 + }, 18 + ]) 19 + 20 + const assistant = messages[0] as (typeof messages)[number] & { reasoning_content?: string } 21 + assert.equal(assistant.role, "assistant") 22 + assert.equal(assistant.reasoning_content, "") 23 + })
+38
src/runner/util.ts
··· 18 18 19 19 export const API_BASE = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1" 20 20 export const MODEL = process.env.MODEL ?? "" 21 + export const PRIMARY_PROVIDER_REQUIRES_REASONING_REPLAY = 22 + API_BASE.toLowerCase().includes("deepseek") || MODEL.toLowerCase().includes("deepseek") 21 23 const DEFAULT_FALLBACK_BASE = "http://localhost:1234/v1" 22 24 const isLikelyLocalBase = (baseUrl: string): boolean => { 23 25 const lowered = baseUrl.trim().toLowerCase() ··· 45 47 process.env.FALLBACK_OPENAI_BASE_URL ?? process.env.OPENROUTER_BASE_URL ?? process.env.LMSTUDIO_BASE_URL ?? DEFAULT_FALLBACK_BASE 46 48 export const FALLBACK_MODEL = 47 49 process.env.FALLBACK_MODEL ?? process.env.OPENROUTER_MODEL ?? process.env.LMSTUDIO_MODEL ?? "zai-org/glm-4.7-flash" 50 + export const FALLBACK_PROVIDER_REQUIRES_REASONING_REPLAY = 51 + FALLBACK_BASE.toLowerCase().includes("deepseek") || FALLBACK_MODEL.toLowerCase().includes("deepseek") 48 52 export const SUMMARY_BASE = 49 53 process.env.SUMMARY_OPENAI_BASE_URL ?? process.env.SUMMARY_BASE_URL ?? "" 50 54 export const SUMMARY_MODEL = process.env.SUMMARY_MODEL ?? "" ··· 467 471 await fs.unlink(SESSION_FILE).catch(() => {}) 468 472 } 469 473 474 + function normalizeReasoningReplay(msgs: Message[]): Message[] { 475 + if (!ENABLE_THINKING) return msgs 476 + const needsReplayNormalization = 477 + PRIMARY_PROVIDER_REQUIRES_REASONING_REPLAY || 478 + FALLBACK_PROVIDER_REQUIRES_REASONING_REPLAY || 479 + msgs.some( 480 + (msg) => 481 + msg.role === "assistant" && 482 + typeof (msg as OpenAI.Chat.ChatCompletionMessage & { reasoning_content?: string }).reasoning_content === "string", 483 + ) 484 + if (!needsReplayNormalization) return msgs 485 + 486 + let changed = false 487 + const normalized = msgs.map((msg) => { 488 + if (msg.role !== "assistant") return msg 489 + 490 + const assistant = msg as OpenAI.Chat.ChatCompletionMessage & { reasoning_content?: string } 491 + if (typeof assistant.reasoning_content === "string") return msg 492 + 493 + changed = true 494 + return { 495 + ...assistant, 496 + reasoning_content: "", 497 + } 498 + }) 499 + 500 + if (changed) { 501 + console.log("[runner] backfilled empty reasoning_content on assistant history for provider compatibility") 502 + } 503 + 504 + return normalized 505 + } 506 + 470 507 /** Move mis-ordered tool responses back into place and synthesize missing ones. */ 471 508 export function sanitizeMessages(msgs: Message[]): Message[] { 509 + msgs = normalizeReasoningReplay(msgs) 472 510 let i = 0 473 511 while (i < msgs.length) { 474 512 const msg = msgs[i]