···22import test from "node:test"
33import { __loopTest } from "./loop.js"
44import type { LoopState } from "./types.js"
55+import type { Message } from "../types.js"
5667function makeState(): LoopState {
78 return {
···17181819test("applyDiscordSendNudge appends a follow-up user nudge for unsent Discord replies", () => {
1920 const state = makeState()
2020- const turnMessages = [
2121+ const turnMessages: Message[] = [
2122 {
2223 role: "user",
2324 content: "[user/discord] [discord/dm] hey are you there",
···60616162test("applyDiscordSendNudge does not fire when discord_send was already called", () => {
6263 const state = makeState()
6363- const turnMessages = [
6464+ const turnMessages: Message[] = [
6465 {
6566 role: "user",
6667 content: "[user/discord] [discord/channel] can you reply",
+6-6
src/runner/loop.ts
···11-import type OpenAI from "openai"
21import { recordMetric } from "../metrics.js"
32import { emit } from "../stream.js"
33+import type { Message } from "../types.js"
44import {
55 CONTEXT_COMPACT_TRIGGER_TOKENS,
66 ENABLE_THINKING,
···8282 * conversational text but did not call discord_send. Injects a system
8383 * nudge so the next turn actually delivers the message.
8484 */
8585-function isDiscordInputMessage(message: OpenAI.Chat.ChatCompletionMessage | OpenAI.Chat.ChatCompletionMessageParam): boolean {
8585+function isDiscordInputMessage(message: Message): boolean {
8686 return message.role === "user" && typeof message.content === "string" && /\[discord\/(?:dm|batch|channel)\]/i.test(message.content)
8787}
88888989function hasDiscordInputForTurn(
9090- conversation: OpenAI.Chat.ChatCompletionMessageParam[],
9191- turnMessages: OpenAI.Chat.ChatCompletionMessage[],
9090+ conversation: Message[],
9191+ turnMessages: Message[],
9292 turnStart: number,
9393): boolean {
9494 if (turnMessages.some(isDiscordInputMessage)) return true
···108108109109function applyDiscordSendNudge(
110110 state: LoopState,
111111- turnMessages: OpenAI.Chat.ChatCompletionMessage[],
112112- turnStart: number,
111111+ turnMessages: Message[],
112112+ turnStart = state.conversation.length,
113113): boolean {
114114 // Check if the assistant is responding to active Discord input, including
115115 // the post-restart case where the triggering user message is already in the
+6-1
src/runner/util.test.ts
···11import assert from "node:assert/strict"
22import test from "node:test"
33+import type OpenAI from "openai"
34import { isTransientTransportError, sanitizeMessages, shouldFallback } from "./util.js"
55+66+type AssistantMessageWithReasoning = OpenAI.Chat.ChatCompletionAssistantMessageParam & {
77+ reasoning_content?: string
88+}
49510test("sanitizeMessages backfills empty reasoning_content for assistant history", () => {
611 const messages = sanitizeMessages([
···1419 content: "reply with reasoning",
1520 refusal: null,
1621 reasoning_content: "thinking...",
1717- },
2222+ } as AssistantMessageWithReasoning,
1823 ])
19242025 const assistant = messages[0] as (typeof messages)[number] & { reasoning_content?: string }