@brianli/kimaki 0.4.72-brianli.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// Scheduled task runner for executing due `send --send-at` jobs in the bot process.
|
|
2
|
+
|
|
3
|
+
import { REST, Routes } from 'discord.js'
|
|
4
|
+
import yaml from 'js-yaml'
|
|
5
|
+
import {
|
|
6
|
+
claimScheduledTaskRunning,
|
|
7
|
+
getDuePlannedScheduledTasks,
|
|
8
|
+
markScheduledTaskCronRescheduled,
|
|
9
|
+
markScheduledTaskCronRetry,
|
|
10
|
+
markScheduledTaskFailed,
|
|
11
|
+
markScheduledTaskOneShotCompleted,
|
|
12
|
+
recoverStaleRunningScheduledTasks,
|
|
13
|
+
type ScheduledTask,
|
|
14
|
+
} from './database.js'
|
|
15
|
+
import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js'
|
|
16
|
+
import { notifyError } from './sentry.js'
|
|
17
|
+
import type { ThreadStartMarker } from './system-message.js'
|
|
18
|
+
import {
|
|
19
|
+
getLocalTimeZone,
|
|
20
|
+
getNextCronRun,
|
|
21
|
+
getPromptPreview,
|
|
22
|
+
parseScheduledTaskPayload,
|
|
23
|
+
} from './task-schedule.js'
|
|
24
|
+
import { createDiscordRest } from './discord-api.js'
|
|
25
|
+
|
|
26
|
+
const taskLogger = createLogger(LogPrefix.TASK)
|
|
27
|
+
|
|
28
|
+
type StartTaskRunnerOptions = {
|
|
29
|
+
token: string
|
|
30
|
+
pollIntervalMs?: number
|
|
31
|
+
staleRunningMs?: number
|
|
32
|
+
dueBatchSize?: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
36
|
+
return typeof value === 'object' && value !== null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseMessageId(value: unknown): string | Error {
|
|
40
|
+
if (!isRecord(value)) {
|
|
41
|
+
return new Error('Discord response is not an object')
|
|
42
|
+
}
|
|
43
|
+
if (typeof value.id !== 'string') {
|
|
44
|
+
return new Error('Discord response is missing message ID')
|
|
45
|
+
}
|
|
46
|
+
return value.id
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function executeThreadScheduledTask({
|
|
50
|
+
rest,
|
|
51
|
+
task,
|
|
52
|
+
payload,
|
|
53
|
+
}: {
|
|
54
|
+
rest: REST
|
|
55
|
+
task: ScheduledTask
|
|
56
|
+
payload: {
|
|
57
|
+
threadId: string
|
|
58
|
+
prompt: string
|
|
59
|
+
agent: string | null
|
|
60
|
+
model: string | null
|
|
61
|
+
username: string | null
|
|
62
|
+
userId: string | null
|
|
63
|
+
}
|
|
64
|
+
}): Promise<void | Error> {
|
|
65
|
+
const marker: ThreadStartMarker = {
|
|
66
|
+
cliThreadPrompt: true,
|
|
67
|
+
scheduledKind: task.schedule_kind,
|
|
68
|
+
scheduledTaskId: task.id,
|
|
69
|
+
...(payload.agent ? { agent: payload.agent } : {}),
|
|
70
|
+
...(payload.model ? { model: payload.model } : {}),
|
|
71
|
+
...(payload.username ? { username: payload.username } : {}),
|
|
72
|
+
...(payload.userId ? { userId: payload.userId } : {}),
|
|
73
|
+
}
|
|
74
|
+
const embed = [{ color: 0x2b2d31, footer: { text: yaml.dump(marker) } }]
|
|
75
|
+
const prefixedPrompt = `» **kimaki-cli:** ${payload.prompt}`
|
|
76
|
+
|
|
77
|
+
const postResult = await rest
|
|
78
|
+
.post(Routes.channelMessages(payload.threadId), {
|
|
79
|
+
body: {
|
|
80
|
+
content: prefixedPrompt,
|
|
81
|
+
embeds: embed,
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
.catch((error) => {
|
|
85
|
+
return new Error(`Failed to post scheduled thread task ${task.id}`, {
|
|
86
|
+
cause: error,
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
if (postResult instanceof Error) {
|
|
91
|
+
return postResult
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function executeChannelScheduledTask({
|
|
96
|
+
rest,
|
|
97
|
+
task,
|
|
98
|
+
payload,
|
|
99
|
+
}: {
|
|
100
|
+
rest: REST
|
|
101
|
+
task: ScheduledTask
|
|
102
|
+
payload: {
|
|
103
|
+
channelId: string
|
|
104
|
+
prompt: string
|
|
105
|
+
name: string | null
|
|
106
|
+
notifyOnly: boolean
|
|
107
|
+
worktreeName: string | null
|
|
108
|
+
agent: string | null
|
|
109
|
+
model: string | null
|
|
110
|
+
username: string | null
|
|
111
|
+
userId: string | null
|
|
112
|
+
}
|
|
113
|
+
}): Promise<void | Error> {
|
|
114
|
+
const marker: ThreadStartMarker | undefined = payload.notifyOnly
|
|
115
|
+
? undefined
|
|
116
|
+
: {
|
|
117
|
+
start: true,
|
|
118
|
+
scheduledKind: task.schedule_kind,
|
|
119
|
+
scheduledTaskId: task.id,
|
|
120
|
+
...(payload.worktreeName ? { worktree: payload.worktreeName } : {}),
|
|
121
|
+
...(payload.agent ? { agent: payload.agent } : {}),
|
|
122
|
+
...(payload.model ? { model: payload.model } : {}),
|
|
123
|
+
...(payload.username ? { username: payload.username } : {}),
|
|
124
|
+
...(payload.userId ? { userId: payload.userId } : {}),
|
|
125
|
+
}
|
|
126
|
+
const embeds = marker
|
|
127
|
+
? [{ color: 0x2b2d31, footer: { text: yaml.dump(marker) } }]
|
|
128
|
+
: undefined
|
|
129
|
+
|
|
130
|
+
const starterResult = await rest
|
|
131
|
+
.post(Routes.channelMessages(payload.channelId), {
|
|
132
|
+
body: {
|
|
133
|
+
content: payload.prompt,
|
|
134
|
+
embeds,
|
|
135
|
+
},
|
|
136
|
+
})
|
|
137
|
+
.catch((error) => {
|
|
138
|
+
return new Error(`Failed to create starter message for task ${task.id}`, {
|
|
139
|
+
cause: error,
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
if (starterResult instanceof Error) {
|
|
144
|
+
return starterResult
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const starterMessageId = parseMessageId(starterResult)
|
|
148
|
+
if (starterMessageId instanceof Error) {
|
|
149
|
+
return new Error(`Invalid starter message response for task ${task.id}`, {
|
|
150
|
+
cause: starterMessageId,
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const threadName = (payload.name || getPromptPreview(payload.prompt)).slice(
|
|
155
|
+
0,
|
|
156
|
+
100,
|
|
157
|
+
)
|
|
158
|
+
const threadResult = await rest
|
|
159
|
+
.post(Routes.threads(payload.channelId, starterMessageId), {
|
|
160
|
+
body: {
|
|
161
|
+
name: threadName,
|
|
162
|
+
auto_archive_duration: 1440,
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
.catch((error) => {
|
|
166
|
+
return new Error(`Failed to create thread for task ${task.id}`, {
|
|
167
|
+
cause: error,
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
if (threadResult instanceof Error) {
|
|
172
|
+
return threadResult
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!payload.userId) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const threadIdResult = parseMessageId(threadResult)
|
|
180
|
+
if (threadIdResult instanceof Error) {
|
|
181
|
+
return new Error(`Invalid thread response for task ${task.id}`, {
|
|
182
|
+
cause: threadIdResult,
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const addMemberResult = await rest
|
|
187
|
+
.put(Routes.threadMembers(threadIdResult, payload.userId))
|
|
188
|
+
.catch((error) => {
|
|
189
|
+
return new Error(
|
|
190
|
+
`Failed to add user to scheduled thread for task ${task.id}`,
|
|
191
|
+
{ cause: error },
|
|
192
|
+
)
|
|
193
|
+
})
|
|
194
|
+
if (addMemberResult instanceof Error) {
|
|
195
|
+
return addMemberResult
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function executeScheduledTask({
|
|
200
|
+
rest,
|
|
201
|
+
task,
|
|
202
|
+
}: {
|
|
203
|
+
rest: REST
|
|
204
|
+
task: ScheduledTask
|
|
205
|
+
}): Promise<void | Error> {
|
|
206
|
+
const payloadResult = parseScheduledTaskPayload(task.payload_json)
|
|
207
|
+
if (payloadResult instanceof Error) {
|
|
208
|
+
return new Error(`Task ${task.id} has invalid payload`, {
|
|
209
|
+
cause: payloadResult,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (payloadResult.kind === 'thread') {
|
|
214
|
+
return executeThreadScheduledTask({
|
|
215
|
+
rest,
|
|
216
|
+
task,
|
|
217
|
+
payload: payloadResult,
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return executeChannelScheduledTask({
|
|
222
|
+
rest,
|
|
223
|
+
task,
|
|
224
|
+
payload: payloadResult,
|
|
225
|
+
})
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function finalizeSuccessfulTask({
|
|
229
|
+
task,
|
|
230
|
+
completedAt,
|
|
231
|
+
}: {
|
|
232
|
+
task: ScheduledTask
|
|
233
|
+
completedAt: Date
|
|
234
|
+
}): Promise<void> {
|
|
235
|
+
if (task.schedule_kind === 'at') {
|
|
236
|
+
await markScheduledTaskOneShotCompleted({ taskId: task.id, completedAt })
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!task.cron_expr) {
|
|
241
|
+
await markScheduledTaskFailed({
|
|
242
|
+
taskId: task.id,
|
|
243
|
+
failedAt: completedAt,
|
|
244
|
+
errorMessage: 'Missing cron expression on cron task',
|
|
245
|
+
})
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const timezone = task.timezone || getLocalTimeZone()
|
|
250
|
+
const nextRunResult = getNextCronRun({
|
|
251
|
+
cronExpr: task.cron_expr,
|
|
252
|
+
timezone,
|
|
253
|
+
from: completedAt,
|
|
254
|
+
})
|
|
255
|
+
if (nextRunResult instanceof Error) {
|
|
256
|
+
await markScheduledTaskFailed({
|
|
257
|
+
taskId: task.id,
|
|
258
|
+
failedAt: completedAt,
|
|
259
|
+
errorMessage: nextRunResult.message,
|
|
260
|
+
})
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await markScheduledTaskCronRescheduled({
|
|
265
|
+
taskId: task.id,
|
|
266
|
+
completedAt,
|
|
267
|
+
nextRunAt: nextRunResult,
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function finalizeFailedTask({
|
|
272
|
+
task,
|
|
273
|
+
failedAt,
|
|
274
|
+
error,
|
|
275
|
+
}: {
|
|
276
|
+
task: ScheduledTask
|
|
277
|
+
failedAt: Date
|
|
278
|
+
error: Error
|
|
279
|
+
}): Promise<void> {
|
|
280
|
+
if (task.schedule_kind === 'cron' && task.cron_expr) {
|
|
281
|
+
const timezone = task.timezone || getLocalTimeZone()
|
|
282
|
+
const nextRunResult = getNextCronRun({
|
|
283
|
+
cronExpr: task.cron_expr,
|
|
284
|
+
timezone,
|
|
285
|
+
from: failedAt,
|
|
286
|
+
})
|
|
287
|
+
if (!(nextRunResult instanceof Error)) {
|
|
288
|
+
await markScheduledTaskCronRetry({
|
|
289
|
+
taskId: task.id,
|
|
290
|
+
failedAt,
|
|
291
|
+
errorMessage: error.message,
|
|
292
|
+
nextRunAt: nextRunResult,
|
|
293
|
+
})
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
await markScheduledTaskFailed({
|
|
299
|
+
taskId: task.id,
|
|
300
|
+
failedAt,
|
|
301
|
+
errorMessage: error.message,
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function processDueTask({
|
|
306
|
+
rest,
|
|
307
|
+
task,
|
|
308
|
+
}: {
|
|
309
|
+
rest: REST
|
|
310
|
+
task: ScheduledTask
|
|
311
|
+
}): Promise<void> {
|
|
312
|
+
const startedAt = new Date()
|
|
313
|
+
const claimed = await claimScheduledTaskRunning({
|
|
314
|
+
taskId: task.id,
|
|
315
|
+
startedAt,
|
|
316
|
+
})
|
|
317
|
+
if (!claimed) {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const executeResult = await executeScheduledTask({ rest, task })
|
|
322
|
+
const finishedAt = new Date()
|
|
323
|
+
|
|
324
|
+
if (executeResult instanceof Error) {
|
|
325
|
+
taskLogger.warn(
|
|
326
|
+
`[task-runner] task ${task.id} failed: ${formatErrorWithStack(executeResult)}`,
|
|
327
|
+
)
|
|
328
|
+
await finalizeFailedTask({
|
|
329
|
+
task,
|
|
330
|
+
failedAt: finishedAt,
|
|
331
|
+
error: executeResult,
|
|
332
|
+
})
|
|
333
|
+
return
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await finalizeSuccessfulTask({ task, completedAt: finishedAt })
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function runTaskRunnerTick({
|
|
340
|
+
rest,
|
|
341
|
+
staleRunningMs,
|
|
342
|
+
dueBatchSize,
|
|
343
|
+
}: {
|
|
344
|
+
rest: REST
|
|
345
|
+
staleRunningMs: number
|
|
346
|
+
dueBatchSize: number
|
|
347
|
+
}): Promise<void> {
|
|
348
|
+
const staleBefore = new Date(Date.now() - staleRunningMs)
|
|
349
|
+
const recoveredCount = await recoverStaleRunningScheduledTasks({
|
|
350
|
+
staleBefore,
|
|
351
|
+
})
|
|
352
|
+
if (recoveredCount > 0) {
|
|
353
|
+
taskLogger.warn(
|
|
354
|
+
`[task-runner] Recovered ${recoveredCount} stale running task(s)`,
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const dueTasks = await getDuePlannedScheduledTasks({
|
|
359
|
+
now: new Date(),
|
|
360
|
+
limit: dueBatchSize,
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
await dueTasks.reduce<Promise<void>>(async (previous, task) => {
|
|
364
|
+
await previous
|
|
365
|
+
await processDueTask({ rest, task })
|
|
366
|
+
}, Promise.resolve())
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function startTaskRunner({
|
|
370
|
+
token,
|
|
371
|
+
pollIntervalMs = 5_000,
|
|
372
|
+
staleRunningMs = 120_000,
|
|
373
|
+
dueBatchSize = 20,
|
|
374
|
+
}: StartTaskRunnerOptions): () => Promise<void> {
|
|
375
|
+
const rest = createDiscordRest(token)
|
|
376
|
+
let stopped = false
|
|
377
|
+
let ticking = false
|
|
378
|
+
let tickPromise: Promise<void> | null = null
|
|
379
|
+
|
|
380
|
+
const tick = async () => {
|
|
381
|
+
if (stopped || ticking) {
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
ticking = true
|
|
386
|
+
const currentTickPromise = runTaskRunnerTick({
|
|
387
|
+
rest,
|
|
388
|
+
staleRunningMs,
|
|
389
|
+
dueBatchSize,
|
|
390
|
+
}).catch((error) => {
|
|
391
|
+
return new Error('Task runner tick failed', { cause: error })
|
|
392
|
+
})
|
|
393
|
+
tickPromise = currentTickPromise.then(() => {
|
|
394
|
+
return
|
|
395
|
+
})
|
|
396
|
+
const runResult = await currentTickPromise
|
|
397
|
+
if (runResult instanceof Error) {
|
|
398
|
+
taskLogger.error(`[task-runner] ${formatErrorWithStack(runResult)}`)
|
|
399
|
+
void notifyError(runResult, 'Task runner tick failed')
|
|
400
|
+
}
|
|
401
|
+
ticking = false
|
|
402
|
+
tickPromise = null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const timer = setInterval(() => {
|
|
406
|
+
void tick()
|
|
407
|
+
}, pollIntervalMs)
|
|
408
|
+
|
|
409
|
+
void tick()
|
|
410
|
+
|
|
411
|
+
taskLogger.log(`[task-runner] started (interval=${pollIntervalMs}ms)`)
|
|
412
|
+
|
|
413
|
+
return async () => {
|
|
414
|
+
if (stopped) {
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
stopped = true
|
|
418
|
+
clearInterval(timer)
|
|
419
|
+
if (tickPromise) {
|
|
420
|
+
await tickPromise
|
|
421
|
+
tickPromise = null
|
|
422
|
+
}
|
|
423
|
+
taskLogger.log('[task-runner] stopped')
|
|
424
|
+
}
|
|
425
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Tests for scheduled task date/cron parsing and UTC validation rules.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'vitest'
|
|
4
|
+
import { parseSendAtValue } from './task-schedule.js'
|
|
5
|
+
|
|
6
|
+
describe('parseSendAtValue', () => {
|
|
7
|
+
test('accepts UTC ISO date ending with Z', () => {
|
|
8
|
+
const now = new Date('2026-02-22T13:00:00Z')
|
|
9
|
+
const result = parseSendAtValue({
|
|
10
|
+
value: '2026-03-01T09:00:00Z',
|
|
11
|
+
now,
|
|
12
|
+
timezone: 'UTC',
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
expect(result).not.toBeInstanceOf(Error)
|
|
16
|
+
if (result instanceof Error) {
|
|
17
|
+
throw result
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
expect(result.scheduleKind).toBe('at')
|
|
21
|
+
expect(result.runAt?.toISOString()).toBe('2026-03-01T09:00:00.000Z')
|
|
22
|
+
expect(result.nextRunAt.toISOString()).toBe('2026-03-01T09:00:00.000Z')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('rejects ISO date with non-UTC offset', () => {
|
|
26
|
+
const now = new Date('2026-02-22T13:00:00Z')
|
|
27
|
+
const result = parseSendAtValue({
|
|
28
|
+
value: '2026-03-01T09:00:00+01:00',
|
|
29
|
+
now,
|
|
30
|
+
timezone: 'UTC',
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(result).toBeInstanceOf(Error)
|
|
34
|
+
if (result instanceof Error) {
|
|
35
|
+
expect(result.message).toContain('must be UTC ISO format ending with Z')
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('rejects local ISO date without timezone suffix', () => {
|
|
40
|
+
const now = new Date('2026-02-22T13:00:00Z')
|
|
41
|
+
const result = parseSendAtValue({
|
|
42
|
+
value: '2026-03-01T09:00:00',
|
|
43
|
+
now,
|
|
44
|
+
timezone: 'UTC',
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(result).toBeInstanceOf(Error)
|
|
48
|
+
if (result instanceof Error) {
|
|
49
|
+
expect(result.message).toContain('must be UTC ISO format ending with Z')
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('rejects UTC dates in the past', () => {
|
|
54
|
+
const now = new Date('2026-02-22T13:00:00Z')
|
|
55
|
+
const result = parseSendAtValue({
|
|
56
|
+
value: '2026-02-22T12:59:59Z',
|
|
57
|
+
now,
|
|
58
|
+
timezone: 'UTC',
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(result).toBeInstanceOf(Error)
|
|
62
|
+
if (result instanceof Error) {
|
|
63
|
+
expect(result.message).toContain('must be in the future (UTC)')
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('accepts cron expressions', () => {
|
|
68
|
+
const now = new Date('2026-02-22T13:00:00Z')
|
|
69
|
+
const result = parseSendAtValue({
|
|
70
|
+
value: '0 9 * * 1',
|
|
71
|
+
now,
|
|
72
|
+
timezone: 'UTC',
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
expect(result).not.toBeInstanceOf(Error)
|
|
76
|
+
if (result instanceof Error) {
|
|
77
|
+
throw result
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
expect(result.scheduleKind).toBe('cron')
|
|
81
|
+
expect(result.cronExpr).toBe('0 9 * * 1')
|
|
82
|
+
expect(result.nextRunAt.toISOString()).toBe('2026-02-23T09:00:00.000Z')
|
|
83
|
+
})
|
|
84
|
+
})
|