···
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
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
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
···
116
116
if (!value) return "unknown-time"
117
117
const parsed = new Date(value)
118
118
if (Number.isNaN(parsed.getTime())) return value
119
119
-
return parsed.toISOString().replace("T", " ").replace(".000Z", "Z")
119
119
+
return parsed.toLocaleString("en-US", {
120
120
+
year: "numeric",
121
121
+
month: "short",
122
122
+
day: "numeric",
123
123
+
hour: "numeric",
124
124
+
minute: "2-digit",
125
125
+
hour12: true,
126
126
+
timeZoneName: "short",
127
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
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
337
-
const autoDemotedCount = autoDemoteStalePendingItems(autoSeenMinutes)
338
338
-
const repairedMessageCount = repairDiscordMessageChannelFlags()
346
346
+
autoDemoteStalePendingItems(autoSeenMinutes)
347
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
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
364
-
`[discord batch] ${from} -> ${nowIso}`,
365
365
-
`new_messages=${recentMessages.length}${truncated ? "+" : ""} channels=${uniqueChannels.size} pending_inbox=${pendingCount} scope=${batchOnlyConfigured ? "configured+dm" : "all"}`,
366
366
-
`auto_seen_timeout=${autoSeenMinutes}m auto_demoted=${autoDemotedCount}`,
367
367
-
`channel_flag_repairs=${repairedMessageCount}`,
372
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
381
-
lines.push(`- [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${fullMessageText(row.content)}${formatBatchImages(row.raw_json)}`)
386
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
398
-
lines.push(`- ${row.item_id} [${row.bucket}] [${label}] [${ts}] ${author}${formatReplyContext(replyTo)}: ${compactText(row.content, 120)}${formatBatchImages(row.raw_json)}`)
403
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
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
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
436
-
if (!sourceItemId?.trim()) return null
437
437
-
return getItemMessageId(sourceItemId.trim())
441
441
+
return resolveSourceMessageId(sourceItemId)
442
442
+
}
443
443
+
444
444
+
function resolveSourceMessageId(sourceItemId?: string): string | null {
445
445
+
const safeSourceItemId = sourceItemId?.trim()
446
446
+
if (!safeSourceItemId) return null
447
447
+
return getItemMessageId(safeSourceItemId) ?? (messageExistsById(safeSourceItemId) ? safeSourceItemId : null)
448
448
+
}
449
449
+
450
450
+
function resolveSourceChannelId(sourceItemId?: string): string | null {
451
451
+
const sourceMessageId = resolveSourceMessageId(sourceItemId)
452
452
+
if (!sourceMessageId) return null
453
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
497
-
channelId = getItemChannelId(params.sourceItemId.trim()) ?? ""
513
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
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
550
-
if (resolvedSourceItemId) {
567
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
584
+
resolved_source_message_id: resolvedSourceMessageId,
567
585
inferred_source_item_id: inferredSourceItemId,
568
586
stored: ingest.stored,
569
587
}
···
24
24
channel messages are context, not direct requests. replying is optional; use judgment.
25
25
26
26
recent messages:
27
27
-
- [channel/staying up till 1 billion oclock/#niri] [2026-05-01 03:11:14.553Z] @meowskullz: awa
27
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
30
-
- 1499679672404021248 [mention] [channel/staying up till 1 billion oclock/#niri] [2026-05-01 03:11:14.553Z] @meowskullz: awa`
30
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
66
-
- [channel/meowskullz's server/#ai-sister-yap] [2026-05-01 08:01:35.639Z] @rose: foxie emoji
66
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)`
···
1
1
+
import assert from "node:assert/strict"
2
2
+
import test from "node:test"
3
3
+
import type { Message } from "../types"
4
4
+
import { __toolRegistryTest } from "./loop-tool-registry"
5
5
+
6
6
+
function assistantTool(name: string, id: string, args = "{}"): Message {
7
7
+
return {
8
8
+
role: "assistant",
9
9
+
content: null,
10
10
+
tool_calls: [
11
11
+
{
12
12
+
id,
13
13
+
type: "function",
14
14
+
function: {
15
15
+
name,
16
16
+
arguments: args,
17
17
+
},
18
18
+
},
19
19
+
],
20
20
+
}
21
21
+
}
22
22
+
23
23
+
function toolResult(id: string, content: string): Message {
24
24
+
return {
25
25
+
role: "tool",
26
26
+
tool_call_id: id,
27
27
+
content,
28
28
+
}
29
29
+
}
30
30
+
31
31
+
test("discord_send without channel only accepts source_item_id from latest Discord target context", () => {
32
32
+
const conversation: Message[] = [
33
33
+
{
34
34
+
role: "user",
35
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
36
+
},
37
37
+
assistantTool("discord_backread", "call_dm"),
38
38
+
toolResult("call_dm", '[{"source_item_id":"dm1","message_id":"dm1","channel_id":"dm-channel"}]'),
39
39
+
assistantTool("discord_backread", "call_channel"),
40
40
+
toolResult("call_channel", '[{"source_item_id":"chan1","message_id":"chan1","channel_id":"server-channel"}]'),
41
41
+
]
42
42
+
43
43
+
const dmError = __toolRegistryTest.validateNoChannelDiscordSendTarget({ conversation }, undefined, "dm1")
44
44
+
assert.ok(dmError)
45
45
+
assert.match(dmError, /latest Discord target context/)
46
46
+
assert.equal(
47
47
+
__toolRegistryTest.validateNoChannelDiscordSendTarget({ conversation }, undefined, "chan1"),
48
48
+
null,
49
49
+
)
50
50
+
})
51
51
+
52
52
+
test("discord_send without channel accepts current Discord event source_item_id", () => {
53
53
+
const conversation: Message[] = [
54
54
+
{
55
55
+
role: "user",
56
56
+
content: "[discord/channel] @nova\ncontext: server/#room\nmessage_id: 123\nsource_item_id: 123\n\naurora",
57
57
+
},
58
58
+
]
59
59
+
60
60
+
assert.equal(
61
61
+
__toolRegistryTest.validateNoChannelDiscordSendTarget({ conversation }, undefined, "123"),
62
62
+
null,
63
63
+
)
64
64
+
})
65
65
+
66
66
+
test("formatDiscordSendResult emits a compact one-line ack", () => {
67
67
+
const ack = __toolRegistryTest.formatDiscordSendResult({
68
68
+
ok: true,
69
69
+
sent_message_id: "1510087371281010840",
70
70
+
channel_id: "1497733589545123981",
71
71
+
reply_mode: "auto",
72
72
+
used_reference_message_id: null,
73
73
+
resolved_source_item_id: null,
74
74
+
inferred_source_item_id: null,
75
75
+
stored: true,
76
76
+
})
77
77
+
78
78
+
assert.equal(ack, "discord_send ok sent_message_id=1510087371281010840 channel_id=1497733589545123981")
79
79
+
assert.equal(ack.includes("\n"), false)
80
80
+
assert.equal(ack.includes("null"), false)
81
81
+
})
···
13
13
} from "../discord/state"
14
14
import { listAliases, removeAlias, searchMemories, setAlias } from "../memory"
15
15
import { emit } from "../stream"
16
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
22
+
23
23
+
function asRecord(value: unknown): Record<string, unknown> | null {
24
24
+
return value && typeof value === "object" ? (value as Record<string, unknown>) : null
25
25
+
}
26
26
+
27
27
+
function messageContentText(message: Message): string {
28
28
+
const content = (message as { content?: unknown }).content
29
29
+
if (typeof content === "string") return content
30
30
+
if (!Array.isArray(content)) return ""
31
31
+
32
32
+
const chunks: string[] = []
33
33
+
for (const part of content) {
34
34
+
const partRecord = asRecord(part)
35
35
+
if (partRecord?.type === "text" && typeof partRecord.text === "string") chunks.push(partRecord.text)
36
36
+
}
37
37
+
return chunks.join("\n")
38
38
+
}
39
39
+
40
40
+
function isDiscordUserMessage(message: Message): boolean {
41
41
+
return message.role === "user" && /\[discord(?:\/(?:dm|channel)| batch)\]|\[incoming — discord\]/i.test(messageContentText(message))
42
42
+
}
43
43
+
44
44
+
function isInjectedImageUserMessage(message: Message): boolean {
45
45
+
const content = (message as { content?: unknown }).content
46
46
+
return Array.isArray(content) && content.some((part) => asRecord(part)?.type === "image_url")
47
47
+
}
48
48
+
49
49
+
function isInternalUserMessage(message: Message): boolean {
50
50
+
const text = messageContentText(message).trim()
51
51
+
return text.startsWith("[context summary") || text.startsWith("[system]")
52
52
+
}
53
53
+
54
54
+
function toolNameForMessage(conversation: Message[], index: number): string | null {
55
55
+
const message = conversation[index] as (Message & { tool_call_id?: unknown }) | undefined
56
56
+
const toolCallId = typeof message?.tool_call_id === "string" ? message.tool_call_id : ""
57
57
+
if (!toolCallId) return null
58
58
+
59
59
+
for (let i = index - 1; i >= 0; i--) {
60
60
+
const candidate = conversation[i]
61
61
+
if (!candidate || candidate.role !== "assistant") continue
62
62
+
const calls = (candidate as { tool_calls?: unknown }).tool_calls
63
63
+
if (!Array.isArray(calls)) continue
64
64
+
for (const call of calls) {
65
65
+
const callRecord = asRecord(call)
66
66
+
if (callRecord?.id !== toolCallId) continue
67
67
+
const fn = asRecord(callRecord.function)
68
68
+
return typeof fn?.name === "string" ? fn.name : null
69
69
+
}
70
70
+
}
71
71
+
72
72
+
return null
73
73
+
}
74
74
+
75
75
+
function isDiscordTargetContextMessage(conversation: Message[], index: number): boolean {
76
76
+
const message = conversation[index]
77
77
+
if (!message) return false
78
78
+
if (isDiscordUserMessage(message)) return true
79
79
+
if (message.role !== "tool") return false
80
80
+
return ["discord_backread", "discord_inbox"].includes(toolNameForMessage(conversation, index) ?? "")
81
81
+
}
82
82
+
83
83
+
function activeSourceContextStart(conversation: Message[]): number {
84
84
+
let lastUser = -1
85
85
+
let lastDiscordUser = -1
86
86
+
for (let i = conversation.length - 1; i >= 0; i--) {
87
87
+
const message = conversation[i]
88
88
+
if (!message || message.role !== "user") continue
89
89
+
if (lastUser < 0) lastUser = i
90
90
+
if (isDiscordUserMessage(message)) {
91
91
+
lastDiscordUser = i
92
92
+
break
93
93
+
}
94
94
+
}
95
95
+
96
96
+
if (lastUser < 0) return 0
97
97
+
const latestUser = conversation[lastUser]
98
98
+
if (lastDiscordUser >= 0 && latestUser && (isInjectedImageUserMessage(latestUser) || isInternalUserMessage(latestUser))) {
99
99
+
return lastDiscordUser
100
100
+
}
101
101
+
return lastUser
102
102
+
}
103
103
+
104
104
+
function sourceItemIdAppearsInActiveContext(conversation: Message[], sourceItemId: string): boolean {
105
105
+
const safeSourceItemId = sourceItemId.trim()
106
106
+
if (!safeSourceItemId) return false
107
107
+
108
108
+
const pattern = new RegExp(`(^|\\D)${safeSourceItemId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\D|$)`)
109
109
+
const activeStart = activeSourceContextStart(conversation)
110
110
+
let targetStart = activeStart
111
111
+
for (let i = conversation.length - 1; i >= activeStart; i--) {
112
112
+
if (isDiscordTargetContextMessage(conversation, i)) {
113
113
+
targetStart = i
114
114
+
break
115
115
+
}
116
116
+
}
117
117
+
118
118
+
const activeMessages = conversation.slice(targetStart)
119
119
+
return activeMessages.some((message) => {
120
120
+
if (message.role !== "user" && message.role !== "tool") return false
121
121
+
return pattern.test(messageContentText(message))
122
122
+
})
123
123
+
}
124
124
+
125
125
+
function validateNoChannelDiscordSendTarget(state: { conversation: Message[] }, channelId: unknown, sourceItemId: unknown): string | null {
126
126
+
if (typeof channelId === "string" && channelId.trim()) return null
127
127
+
if (typeof sourceItemId !== "string" || !sourceItemId.trim()) return null
128
128
+
if (sourceItemIdAppearsInActiveContext(state.conversation, sourceItemId)) return null
129
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
130
+
}
131
131
+
132
132
+
function resultValue(result: Record<string, unknown>, key: string): string | null {
133
133
+
const value = result[key]
134
134
+
return typeof value === "string" && value.trim() ? value.trim() : null
135
135
+
}
136
136
+
137
137
+
function formatDiscordSendResult(result: Record<string, unknown>): string {
138
138
+
if (result.ok !== true) return JSON.stringify(result, null, 2)
139
139
+
140
140
+
const parts = [
141
141
+
"discord_send ok",
142
142
+
resultValue(result, "sent_message_id") ? `sent_message_id=${resultValue(result, "sent_message_id")}` : null,
143
143
+
resultValue(result, "channel_id") ? `channel_id=${resultValue(result, "channel_id")}` : null,
144
144
+
resultValue(result, "used_reference_message_id") ? `reply_to=${resultValue(result, "used_reference_message_id")}` : null,
145
145
+
resultValue(result, "resolved_source_item_id") ? `source_item_id=${resultValue(result, "resolved_source_item_id")}` : null,
146
146
+
].filter((part): part is string => Boolean(part))
147
147
+
148
148
+
return parts.join(" ")
149
149
+
}
21
150
22
151
/**
23
152
* Builds the function-tool handler table for the runner loop.
···
249
378
},
250
379
}),
251
380
252
252
-
discord_send: (ctx) =>
253
253
-
runStandardTool(ctx, {
381
381
+
discord_send: (ctx) => {
382
382
+
const targetError = validateNoChannelDiscordSendTarget(ctx.state, ctx.args.channel_id, ctx.args.source_item_id)
383
383
+
if (targetError) {
384
384
+
recordToolResult(ctx.convId, ctx.state, ctx.call, "discord_send", { _invalid_target: true }, targetError)
385
385
+
return Promise.resolve({})
386
386
+
}
387
387
+
388
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
257
-
run: async (channel_id, content, source_item_id, reply_mode, reference_message) =>
258
258
-
JSON.stringify(
259
259
-
await sendDiscordMessage({
260
260
-
channelId: channel_id as string,
261
261
-
content: content as string,
262
262
-
sourceItemId: source_item_id as string | undefined,
263
263
-
replyMode: reply_mode as string | undefined,
264
264
-
referenceMessage: reference_message as string | undefined,
265
265
-
}),
266
266
-
null,
267
267
-
2,
268
268
-
),
269
269
-
}),
392
392
+
run: async (channel_id, content, source_item_id, reply_mode, reference_message) => {
393
393
+
const result = await sendDiscordMessage({
394
394
+
channelId: channel_id as string,
395
395
+
content: content as string,
396
396
+
sourceItemId: source_item_id as string | undefined,
397
397
+
replyMode: reply_mode as string | undefined,
398
398
+
referenceMessage: reference_message as string | undefined,
399
399
+
})
400
400
+
return formatDiscordSendResult(result)
401
401
+
},
402
402
+
})
403
403
+
},
270
404
271
405
discord_channels: (ctx) =>
272
406
runStandardTool(ctx, {
···
286
420
}),
287
421
}
288
422
}
423
423
+
424
424
+
export const __toolRegistryTest = {
425
425
+
formatDiscordSendResult,
426
426
+
validateNoChannelDiscordSendTarget,
427
427
+
}
···
427
427
function: {
428
428
name: "discord_send",
429
429
description:
430
430
-
"Send a Discord message. reply_mode=auto sends plain unless conversation continuity is ambiguous, then it uses an explicit reply reference.",
430
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
434
-
channel_id: { type: "string", description: "Target channel id." },
434
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
438
-
description: "Optional inbox item id to mark as acted after sending.",
438
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",
···
8
8
return Number.isNaN(parsed.getTime()) ? fallback : parsed.toISOString()
9
9
}
10
10
11
11
+
function formatDiscordTimestamp(value: string): string {
12
12
+
const parsed = new Date(value)
13
13
+
if (Number.isNaN(parsed.getTime())) return value
14
14
+
return parsed.toLocaleString("en-US", {
15
15
+
year: "numeric",
16
16
+
month: "short",
17
17
+
day: "numeric",
18
18
+
hour: "numeric",
19
19
+
minute: "2-digit",
20
20
+
hour12: true,
21
21
+
timeZoneName: "short",
22
22
+
})
23
23
+
}
24
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
98
-
const timestamp = asIsoTimestamp(message.timestamp ?? b.timestamp, triggeredAt)
112
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
115
-
content: `${header} @${authorName}\ncontext: ${location}\nmessage_id: ${messageId}\ntimestamp: ${timestamp}${replyLine ? `\n${replyLine}` : ""}\naction: ${action}\n\n${content}${imageBlock}`,
129
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
}