This repository has no description
0

Configure Feed

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

kimi is a funny model

+291 -39
+1 -1
src/bootstrap.ts
··· 104 104 105 105 **IMPORTANT: Writing text in your message content does NOT send it to Discord. You must call \`discord_send\` to actually deliver a message.** 106 106 107 - - \`discord_send\`: send a message to a Discord channel or DM. requires \`channel_id\` and \`content\`. use \`source_item_id\` to mark the inbox item as acted in the same call. 107 + - \`discord_send\`: send a message to a Discord channel or DM. requires \`content\` plus either \`channel_id\` or a \`source_item_id\` from the target Discord message. Prefer \`source_item_id\` for replies; it routes to that message's channel and marks inbox items acted when applicable. 108 108 - \`discord_inbox\`: list pending Discord inbox items (messages waiting for your attention) 109 109 - \`discord_backread\`: read message history for a channel 110 110 - \`discord_scan\`: scan configured channels and ingest new messages into the inbox
+33 -15
src/discord/state.ts
··· 116 116 if (!value) return "unknown-time" 117 117 const parsed = new Date(value) 118 118 if (Number.isNaN(parsed.getTime())) return value 119 - return parsed.toISOString().replace("T", " ").replace(".000Z", "Z") 119 + return parsed.toLocaleString("en-US", { 120 + year: "numeric", 121 + month: "short", 122 + day: "numeric", 123 + hour: "numeric", 124 + minute: "2-digit", 125 + hour12: true, 126 + timeZoneName: "short", 127 + }) 120 128 } 121 129 122 130 function formatBatchImages(rawJson: string): string { ··· 237 245 const replyByMessageId = buildReplyTargetContextMap(rows) 238 246 return rows.map(({ raw_json: _rawJson, ...row }) => ({ 239 247 ...row, 248 + source_item_id: row.message_id, 240 249 created_at: formatHumanTimestamp(row.created_at), 241 250 ...(replyByMessageId.has(row.message_id) ? { reply_to: replyByMessageId.get(row.message_id) } : {}), 242 251 })) ··· 334 343 : "" 335 344 const botUserId = process.env.DISCORD_BOT_USER_ID?.trim() ?? "" 336 345 337 - const autoDemotedCount = autoDemoteStalePendingItems(autoSeenMinutes) 338 - const repairedMessageCount = repairDiscordMessageChannelFlags() 346 + autoDemoteStalePendingItems(autoSeenMinutes) 347 + repairDiscordMessageChannelFlags() 339 348 340 349 const from = 341 350 getDiscordMeta("discord_batch_last_dispatched_at") ?? ··· 354 363 355 364 const pendingPreview = queryBatchPendingPreview({ ...queryOpts, limit: previewLimit }) 356 365 357 - const uniqueChannels = new Set(recentMessages.map((row) => row.channel_id)) 358 366 const replyContextByMessageId = buildReplyTargetContextMap([...recentMessages, ...pendingPreview]) 359 367 const hasImages = [...recentMessages, ...pendingPreview].some( 360 368 (row) => extractImageAttachmentsFromRawJson(row.raw_json).length > 0, 361 369 ) 362 370 363 371 const lines: string[] = [ 364 - `[discord batch] ${from} -> ${nowIso}`, 365 - `new_messages=${recentMessages.length}${truncated ? "+" : ""} channels=${uniqueChannels.size} pending_inbox=${pendingCount} scope=${batchOnlyConfigured ? "configured+dm" : "all"}`, 366 - `auto_seen_timeout=${autoSeenMinutes}m auto_demoted=${autoDemotedCount}`, 367 - `channel_flag_repairs=${repairedMessageCount}`, 372 + `[discord batch] ${formatBatchTimestamp(from)} -> ${formatBatchTimestamp(nowIso)}`, 368 373 "channel messages are context, not direct requests. replying is optional; use judgment.", 369 374 ...(hasImages 370 375 ? ["image attachments appear as [images: <discord cdn urls>]; download with shell, then inspect with image_tool if useful."] ··· 378 383 const author = row.author_username ? `@${row.author_username}` : "@unknown" 379 384 const ts = formatBatchTimestamp(row.created_at) 380 385 const replyTo = replyContextByMessageId.get(row.message_id) 381 - lines.push(`- [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${fullMessageText(row.content)}${formatBatchImages(row.raw_json)}`) 386 + lines.push(`- source_item_id=${row.message_id} [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${fullMessageText(row.content)}${formatBatchImages(row.raw_json)}`) 382 387 } 383 388 384 389 if (truncated) { ··· 395 400 const author = row.author_username ? `@${row.author_username}` : "@unknown" 396 401 const ts = formatBatchTimestamp(row.created_at) 397 402 const replyTo = replyContextByMessageId.get(row.message_id) 398 - lines.push(`- ${row.item_id} [${row.bucket}] [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${compactText(row.content, 120)}${formatBatchImages(row.raw_json)}`) 403 + lines.push(`- source_item_id=${row.item_id} bucket=${row.bucket} [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${compactText(row.content, 120)}${formatBatchImages(row.raw_json)}`) 399 404 } 400 405 } 401 406 402 407 lines.push("") 403 - lines.push("you can reply if useful via discord_send, or choose not to reply. mark decisions with discord_mark when you handle pending items.") 408 + lines.push("you can reply if useful via discord_send using source_item_id from the target message, or choose not to reply. mark decisions with discord_mark when you handle pending items.") 404 409 405 410 setDiscordMeta("discord_batch_last_dispatched_at", nowIso) 406 411 ··· 433 438 return null 434 439 } 435 440 436 - if (!sourceItemId?.trim()) return null 437 - return getItemMessageId(sourceItemId.trim()) 441 + return resolveSourceMessageId(sourceItemId) 442 + } 443 + 444 + function resolveSourceMessageId(sourceItemId?: string): string | null { 445 + const safeSourceItemId = sourceItemId?.trim() 446 + if (!safeSourceItemId) return null 447 + return getItemMessageId(safeSourceItemId) ?? (messageExistsById(safeSourceItemId) ? safeSourceItemId : null) 448 + } 449 + 450 + function resolveSourceChannelId(sourceItemId?: string): string | null { 451 + const sourceMessageId = resolveSourceMessageId(sourceItemId) 452 + if (!sourceMessageId) return null 453 + return getMessageForReference(sourceMessageId)?.channel_id?.trim() || null 438 454 } 439 455 440 456 const AUTO_REPLY_STALE_MINUTES = 10 ··· 494 510 }): Promise<Record<string, unknown>> { 495 511 let channelId = String(params.channelId ?? "").trim() 496 512 if (!channelId && params.sourceItemId?.trim()) { 497 - channelId = getItemChannelId(params.sourceItemId.trim()) ?? "" 513 + channelId = getItemChannelId(params.sourceItemId.trim()) ?? resolveSourceChannelId(params.sourceItemId) ?? "" 498 514 } 499 515 if (!channelId) throw new Error("channel_id is required (or provide source_item_id that maps to one)") 500 516 ··· 505 521 const explicitSourceItemId = params.sourceItemId?.trim() ? params.sourceItemId.trim() : null 506 522 const inferredSourceItemId = explicitSourceItemId ? null : inferPendingDmItemId(channelId) 507 523 const resolvedSourceItemId = explicitSourceItemId ?? inferredSourceItemId 524 + const resolvedSourceMessageId = resolveSourceMessageId(resolvedSourceItemId ?? undefined) 508 525 const referenceMessageId = await chooseMessageReference({ 509 526 channelId, 510 527 replyMode, ··· 547 564 { botUserId }, 548 565 ) 549 566 550 - if (resolvedSourceItemId) { 567 + if (resolvedSourceItemId && getItemMessageId(resolvedSourceItemId)) { 551 568 const wasInferred = !explicitSourceItemId 552 569 markDiscordItem( 553 570 resolvedSourceItemId, ··· 564 581 reply_mode: replyMode, 565 582 used_reference_message_id: referenceMessageId, 566 583 resolved_source_item_id: resolvedSourceItemId, 584 + resolved_source_message_id: resolvedSourceMessageId, 567 585 inferred_source_item_id: inferredSourceItemId, 568 586 stored: ingest.stored, 569 587 }
+3 -3
src/memory.test.ts
··· 24 24 channel messages are context, not direct requests. replying is optional; use judgment. 25 25 26 26 recent messages: 27 - - [channel/staying up till 1 billion oclock/#niri] [2026-05-01 03:11:14.553Z] @meowskullz: awa 27 + - source_item_id=1499679672404021248 [channel/staying up till 1 billion oclock/#niri] [2026-05-01 03:11:14.553Z] @meowskullz: awa 28 28 29 29 pending preview: 30 - - 1499679672404021248 [mention] [channel/staying up till 1 billion oclock/#niri] [2026-05-01 03:11:14.553Z] @meowskullz: awa` 30 + - source_item_id=1499679672404021248 bucket=mention [channel/staying up till 1 billion oclock/#niri] [2026-05-01 03:11:14.553Z] @meowskullz: awa` 31 31 32 32 assert.deepEqual(__memoryTest.memoryQueryForUserMessage(batch), { 33 33 sender: "meowskullz", ··· 63 63 new_messages=1 channels=1 pending_inbox=0 scope=configured+dm 64 64 65 65 recent messages: 66 - - [channel/meowskullz's server/#ai-sister-yap] [2026-05-01 08:01:35.639Z] @rose: foxie emoji 66 + - source_item_id=1499680000000000000 [channel/meowskullz's server/#ai-sister-yap] [2026-05-01 08:01:35.639Z] @rose: foxie emoji 67 67 68 68 pending preview: 69 69 - (none)`
+81
src/runner/loop-tool-registry.test.ts
··· 1 + import assert from "node:assert/strict" 2 + import test from "node:test" 3 + import type { Message } from "../types" 4 + import { __toolRegistryTest } from "./loop-tool-registry" 5 + 6 + function assistantTool(name: string, id: string, args = "{}"): Message { 7 + return { 8 + role: "assistant", 9 + content: null, 10 + tool_calls: [ 11 + { 12 + id, 13 + type: "function", 14 + function: { 15 + name, 16 + arguments: args, 17 + }, 18 + }, 19 + ], 20 + } 21 + } 22 + 23 + function toolResult(id: string, content: string): Message { 24 + return { 25 + role: "tool", 26 + tool_call_id: id, 27 + content, 28 + } 29 + } 30 + 31 + test("discord_send without channel only accepts source_item_id from latest Discord target context", () => { 32 + const conversation: Message[] = [ 33 + { 34 + role: "user", 35 + content: "[incoming — discord]\n\n[discord batch]\nrecent messages:\n- source_item_id=chan0 [channel/server/#room] [May 29, 2026, 9:08 PM EDT] @nova: hi", 36 + }, 37 + assistantTool("discord_backread", "call_dm"), 38 + toolResult("call_dm", '[{"source_item_id":"dm1","message_id":"dm1","channel_id":"dm-channel"}]'), 39 + assistantTool("discord_backread", "call_channel"), 40 + toolResult("call_channel", '[{"source_item_id":"chan1","message_id":"chan1","channel_id":"server-channel"}]'), 41 + ] 42 + 43 + const dmError = __toolRegistryTest.validateNoChannelDiscordSendTarget({ conversation }, undefined, "dm1") 44 + assert.ok(dmError) 45 + assert.match(dmError, /latest Discord target context/) 46 + assert.equal( 47 + __toolRegistryTest.validateNoChannelDiscordSendTarget({ conversation }, undefined, "chan1"), 48 + null, 49 + ) 50 + }) 51 + 52 + test("discord_send without channel accepts current Discord event source_item_id", () => { 53 + const conversation: Message[] = [ 54 + { 55 + role: "user", 56 + content: "[discord/channel] @nova\ncontext: server/#room\nmessage_id: 123\nsource_item_id: 123\n\naurora", 57 + }, 58 + ] 59 + 60 + assert.equal( 61 + __toolRegistryTest.validateNoChannelDiscordSendTarget({ conversation }, undefined, "123"), 62 + null, 63 + ) 64 + }) 65 + 66 + test("formatDiscordSendResult emits a compact one-line ack", () => { 67 + const ack = __toolRegistryTest.formatDiscordSendResult({ 68 + ok: true, 69 + sent_message_id: "1510087371281010840", 70 + channel_id: "1497733589545123981", 71 + reply_mode: "auto", 72 + used_reference_message_id: null, 73 + resolved_source_item_id: null, 74 + inferred_source_item_id: null, 75 + stored: true, 76 + }) 77 + 78 + assert.equal(ack, "discord_send ok sent_message_id=1510087371281010840 channel_id=1497733589545123981") 79 + assert.equal(ack.includes("\n"), false) 80 + assert.equal(ack.includes("null"), false) 81 + })
+154 -15
src/runner/loop-tool-registry.ts
··· 13 13 } from "../discord/state" 14 14 import { listAliases, removeAlias, searchMemories, setAlias } from "../memory" 15 15 import { emit } from "../stream" 16 + import type { Message } from "../types" 16 17 import type { ToolHandler } from "./loop-shared" 17 18 import { pushToolMessage, recordToolResult, runStandardTool, toolError } from "./loop-tool-runtime" 18 19 import { parseImageDetail, saveRestSnapshot } from "./util" 19 20 20 21 const DEFAULT_WAIT_THEN_CONTINUE_MS = 10_000 22 + 23 + function asRecord(value: unknown): Record<string, unknown> | null { 24 + return value && typeof value === "object" ? (value as Record<string, unknown>) : null 25 + } 26 + 27 + function messageContentText(message: Message): string { 28 + const content = (message as { content?: unknown }).content 29 + if (typeof content === "string") return content 30 + if (!Array.isArray(content)) return "" 31 + 32 + const chunks: string[] = [] 33 + for (const part of content) { 34 + const partRecord = asRecord(part) 35 + if (partRecord?.type === "text" && typeof partRecord.text === "string") chunks.push(partRecord.text) 36 + } 37 + return chunks.join("\n") 38 + } 39 + 40 + function isDiscordUserMessage(message: Message): boolean { 41 + return message.role === "user" && /\[discord(?:\/(?:dm|channel)| batch)\]|\[incoming — discord\]/i.test(messageContentText(message)) 42 + } 43 + 44 + function isInjectedImageUserMessage(message: Message): boolean { 45 + const content = (message as { content?: unknown }).content 46 + return Array.isArray(content) && content.some((part) => asRecord(part)?.type === "image_url") 47 + } 48 + 49 + function isInternalUserMessage(message: Message): boolean { 50 + const text = messageContentText(message).trim() 51 + return text.startsWith("[context summary") || text.startsWith("[system]") 52 + } 53 + 54 + function toolNameForMessage(conversation: Message[], index: number): string | null { 55 + const message = conversation[index] as (Message & { tool_call_id?: unknown }) | undefined 56 + const toolCallId = typeof message?.tool_call_id === "string" ? message.tool_call_id : "" 57 + if (!toolCallId) return null 58 + 59 + for (let i = index - 1; i >= 0; i--) { 60 + const candidate = conversation[i] 61 + if (!candidate || candidate.role !== "assistant") continue 62 + const calls = (candidate as { tool_calls?: unknown }).tool_calls 63 + if (!Array.isArray(calls)) continue 64 + for (const call of calls) { 65 + const callRecord = asRecord(call) 66 + if (callRecord?.id !== toolCallId) continue 67 + const fn = asRecord(callRecord.function) 68 + return typeof fn?.name === "string" ? fn.name : null 69 + } 70 + } 71 + 72 + return null 73 + } 74 + 75 + function isDiscordTargetContextMessage(conversation: Message[], index: number): boolean { 76 + const message = conversation[index] 77 + if (!message) return false 78 + if (isDiscordUserMessage(message)) return true 79 + if (message.role !== "tool") return false 80 + return ["discord_backread", "discord_inbox"].includes(toolNameForMessage(conversation, index) ?? "") 81 + } 82 + 83 + function activeSourceContextStart(conversation: Message[]): number { 84 + let lastUser = -1 85 + let lastDiscordUser = -1 86 + for (let i = conversation.length - 1; i >= 0; i--) { 87 + const message = conversation[i] 88 + if (!message || message.role !== "user") continue 89 + if (lastUser < 0) lastUser = i 90 + if (isDiscordUserMessage(message)) { 91 + lastDiscordUser = i 92 + break 93 + } 94 + } 95 + 96 + if (lastUser < 0) return 0 97 + const latestUser = conversation[lastUser] 98 + if (lastDiscordUser >= 0 && latestUser && (isInjectedImageUserMessage(latestUser) || isInternalUserMessage(latestUser))) { 99 + return lastDiscordUser 100 + } 101 + return lastUser 102 + } 103 + 104 + function sourceItemIdAppearsInActiveContext(conversation: Message[], sourceItemId: string): boolean { 105 + const safeSourceItemId = sourceItemId.trim() 106 + if (!safeSourceItemId) return false 107 + 108 + const pattern = new RegExp(`(^|\\D)${safeSourceItemId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\D|$)`) 109 + const activeStart = activeSourceContextStart(conversation) 110 + let targetStart = activeStart 111 + for (let i = conversation.length - 1; i >= activeStart; i--) { 112 + if (isDiscordTargetContextMessage(conversation, i)) { 113 + targetStart = i 114 + break 115 + } 116 + } 117 + 118 + const activeMessages = conversation.slice(targetStart) 119 + return activeMessages.some((message) => { 120 + if (message.role !== "user" && message.role !== "tool") return false 121 + return pattern.test(messageContentText(message)) 122 + }) 123 + } 124 + 125 + function validateNoChannelDiscordSendTarget(state: { conversation: Message[] }, channelId: unknown, sourceItemId: unknown): string | null { 126 + if (typeof channelId === "string" && channelId.trim()) return null 127 + if (typeof sourceItemId !== "string" || !sourceItemId.trim()) return null 128 + if (sourceItemIdAppearsInActiveContext(state.conversation, sourceItemId)) return null 129 + return "error: source_item_id is not in the latest Discord target context. Use a source_item_id shown in the current Discord message, latest discord_inbox, or latest discord_backread output; otherwise provide channel_id." 130 + } 131 + 132 + function resultValue(result: Record<string, unknown>, key: string): string | null { 133 + const value = result[key] 134 + return typeof value === "string" && value.trim() ? value.trim() : null 135 + } 136 + 137 + function formatDiscordSendResult(result: Record<string, unknown>): string { 138 + if (result.ok !== true) return JSON.stringify(result, null, 2) 139 + 140 + const parts = [ 141 + "discord_send ok", 142 + resultValue(result, "sent_message_id") ? `sent_message_id=${resultValue(result, "sent_message_id")}` : null, 143 + resultValue(result, "channel_id") ? `channel_id=${resultValue(result, "channel_id")}` : null, 144 + resultValue(result, "used_reference_message_id") ? `reply_to=${resultValue(result, "used_reference_message_id")}` : null, 145 + resultValue(result, "resolved_source_item_id") ? `source_item_id=${resultValue(result, "resolved_source_item_id")}` : null, 146 + ].filter((part): part is string => Boolean(part)) 147 + 148 + return parts.join(" ") 149 + } 21 150 22 151 /** 23 152 * Builds the function-tool handler table for the runner loop. ··· 249 378 }, 250 379 }), 251 380 252 - discord_send: (ctx) => 253 - runStandardTool(ctx, { 381 + discord_send: (ctx) => { 382 + const targetError = validateNoChannelDiscordSendTarget(ctx.state, ctx.args.channel_id, ctx.args.source_item_id) 383 + if (targetError) { 384 + recordToolResult(ctx.convId, ctx.state, ctx.call, "discord_send", { _invalid_target: true }, targetError) 385 + return Promise.resolve({}) 386 + } 387 + 388 + return runStandardTool(ctx, { 254 389 name: "discord_send", 255 390 logArgKeys: ["channel_id", "source_item_id", "reply_mode"] as const, 256 391 runArgKeys: ["channel_id", "content", "source_item_id", "reply_mode", "reference_message"] as const, 257 - run: async (channel_id, content, source_item_id, reply_mode, reference_message) => 258 - JSON.stringify( 259 - await sendDiscordMessage({ 260 - channelId: channel_id as string, 261 - content: content as string, 262 - sourceItemId: source_item_id as string | undefined, 263 - replyMode: reply_mode as string | undefined, 264 - referenceMessage: reference_message as string | undefined, 265 - }), 266 - null, 267 - 2, 268 - ), 269 - }), 392 + run: async (channel_id, content, source_item_id, reply_mode, reference_message) => { 393 + const result = await sendDiscordMessage({ 394 + channelId: channel_id as string, 395 + content: content as string, 396 + sourceItemId: source_item_id as string | undefined, 397 + replyMode: reply_mode as string | undefined, 398 + referenceMessage: reference_message as string | undefined, 399 + }) 400 + return formatDiscordSendResult(result) 401 + }, 402 + }) 403 + }, 270 404 271 405 discord_channels: (ctx) => 272 406 runStandardTool(ctx, { ··· 286 420 }), 287 421 } 288 422 } 423 + 424 + export const __toolRegistryTest = { 425 + formatDiscordSendResult, 426 + validateNoChannelDiscordSendTarget, 427 + }
+3 -3
src/runner/util.ts
··· 427 427 function: { 428 428 name: "discord_send", 429 429 description: 430 - "Send a Discord message. reply_mode=auto sends plain unless conversation continuity is ambiguous, then it uses an explicit reply reference.", 430 + "Send a Discord message. Provide content plus either channel_id or source_item_id. Prefer source_item_id from the target Discord message; it routes to that message's channel. reply_mode=auto sends plain unless conversation continuity is ambiguous, then it uses an explicit reply reference.", 431 431 parameters: { 432 432 type: "object", 433 433 properties: { 434 - channel_id: { type: "string", description: "Target channel id." }, 434 + channel_id: { type: "string", description: "Optional target channel id. You can omit this when source_item_id is provided." }, 435 435 content: { type: "string", description: "Message content to send." }, 436 436 source_item_id: { 437 437 type: "string", 438 - description: "Optional inbox item id to mark as acted after sending.", 438 + description: "Inbox item id or Discord message_id from the target message. Use this instead of channel_id for replies; it resolves the destination channel and marks inbox items acted when applicable.", 439 439 }, 440 440 reference_message: { 441 441 type: "string",
+16 -2
src/triggers/discord.ts
··· 8 8 return Number.isNaN(parsed.getTime()) ? fallback : parsed.toISOString() 9 9 } 10 10 11 + function formatDiscordTimestamp(value: string): string { 12 + const parsed = new Date(value) 13 + if (Number.isNaN(parsed.getTime())) return value 14 + return parsed.toLocaleString("en-US", { 15 + year: "numeric", 16 + month: "short", 17 + day: "numeric", 18 + hour: "numeric", 19 + minute: "2-digit", 20 + hour12: true, 21 + timeZoneName: "short", 22 + }) 23 + } 24 + 11 25 const asRecord = asObject 12 26 13 27 function referencedMessageId(message: Record<string, unknown>, body: Record<string, unknown>): string | null { ··· 95 109 String(message.content ?? b.content ?? "").trim() || 96 110 "(no text content)" 97 111 const triggeredAt = new Date().toISOString() 98 - const timestamp = asIsoTimestamp(message.timestamp ?? b.timestamp, triggeredAt) 112 + const timestamp = formatDiscordTimestamp(asIsoTimestamp(message.timestamp ?? b.timestamp, triggeredAt)) 99 113 const authorName = String(author?.global_name ?? author?.username ?? b.author_username ?? b.author ?? "unknown") 100 114 const channelId = String(message.channel_id ?? b.channel_id ?? channel?.id ?? "unknown") 101 115 const messageId = String(message.id ?? b.message_id ?? "unknown") ··· 112 126 return { 113 127 source: "discord", 114 128 triggeredAt, 115 - content: `${header} @${authorName}\ncontext: ${location}\nmessage_id: ${messageId}\ntimestamp: ${timestamp}${replyLine ? `\n${replyLine}` : ""}\naction: ${action}\n\n${content}${imageBlock}`, 129 + content: `${header} @${authorName}\ncontext: ${location}\nmessage_id: ${messageId}\nsource_item_id: ${messageId}\ntimestamp: ${timestamp}${replyLine ? `\n${replyLine}` : ""}\naction: ${action}\n\n${content}${imageBlock}`, 116 130 raw: body, 117 131 } 118 132 }