kimaki 1.3.0 → 1.5.0
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/assets/fonts/Geist-Bold.ttf +0 -0
- package/assets/fonts/Geist-Regular.ttf +0 -0
- package/assets/fonts/GeistMono-Regular.ttf +0 -0
- package/assets/fonts/OFL.txt +92 -0
- package/dist/agent-ui.e2e.test.js +27 -13
- package/dist/agent-ui.e2e.test.js.map +1 -1
- package/dist/agent-ui.js +2 -1
- package/dist/agent-ui.js.map +1 -1
- package/dist/analytics.e2e.test.js +1 -1
- package/dist/analytics.e2e.test.js.map +1 -1
- package/dist/autostart.d.ts +4 -0
- package/dist/autostart.js +55 -10
- package/dist/autostart.js.map +1 -1
- package/dist/bot.d.ts +13 -6
- package/dist/bot.js +17 -15
- package/dist/bot.js.map +1 -1
- package/dist/btw.e2e.test.js +3 -3
- package/dist/builtin-skills.d.ts +3 -0
- package/dist/builtin-skills.js +29 -0
- package/dist/builtin-skills.js.map +1 -1
- package/dist/builtin-skills.test.js +72 -1
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/catalog-refresh.e2e.test.js +2 -2
- package/dist/cli/bot.d.ts +1 -1
- package/dist/cli/bot.js +33 -18
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli/discord.js +32 -6
- package/dist/cli/discord.js.map +1 -1
- package/dist/cli/schedule.js +23 -9
- package/dist/cli/schedule.js.map +1 -1
- package/dist/cli/session.js +37 -12
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/shared.d.ts +5 -3
- package/dist/cli/shared.js +45 -10
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli-discord.e2e.test.js +13 -8
- package/dist/cli-discord.e2e.test.js.map +1 -1
- package/dist/cli-lock.e2e.test.js +87 -20
- package/dist/cli-lock.e2e.test.js.map +1 -1
- package/dist/cli-read.e2e.test.js +18 -9
- package/dist/cli-read.e2e.test.js.map +1 -1
- package/dist/cli-sessions.e2e.test.js +4 -3
- package/dist/cli-sessions.e2e.test.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/preference-commands.d.ts +4 -0
- package/dist/commands/preference-commands.js +38 -4
- package/dist/commands/preference-commands.js.map +1 -1
- package/dist/commands/session-commands.js +11 -8
- package/dist/commands/session-commands.js.map +1 -1
- package/dist/commands/thread-commands.js +94 -24
- package/dist/commands/thread-commands.js.map +1 -1
- package/dist/commands/worktree-commands.d.ts +5 -6
- package/dist/commands/worktree-commands.js +8 -7
- package/dist/commands/worktree-commands.js.map +1 -1
- package/dist/commands.e2e.test.js +25 -21
- package/dist/commands.e2e.test.js.map +1 -1
- package/dist/coordinator-agents.e2e.test.d.ts +1 -0
- package/dist/coordinator-agents.e2e.test.js +113 -0
- package/dist/coordinator-agents.e2e.test.js.map +1 -0
- package/dist/coordinator-idle.e2e.test.d.ts +1 -0
- package/dist/coordinator-idle.e2e.test.js +100 -0
- package/dist/coordinator-idle.e2e.test.js.map +1 -0
- package/dist/coordinator-queue.e2e.test.d.ts +1 -0
- package/dist/coordinator-queue.e2e.test.js +244 -0
- package/dist/coordinator-queue.e2e.test.js.map +1 -0
- package/dist/coordinator-warmup.e2e.test.d.ts +1 -0
- package/dist/coordinator-warmup.e2e.test.js +131 -0
- package/dist/coordinator-warmup.e2e.test.js.map +1 -0
- package/dist/coordinator.d.ts +6 -0
- package/dist/coordinator.e2e.test.d.ts +1 -0
- package/dist/coordinator.e2e.test.js +505 -0
- package/dist/coordinator.e2e.test.js.map +1 -0
- package/dist/coordinator.js +164 -0
- package/dist/coordinator.js.map +1 -0
- package/dist/db.d.ts +78 -3
- package/dist/db.test.js +1 -0
- package/dist/db.test.js.map +1 -1
- package/dist/diagram.d.ts +11 -0
- package/dist/diagram.js +232 -0
- package/dist/diagram.js.map +1 -0
- package/dist/diagram.test.d.ts +1 -0
- package/dist/diagram.test.js +52 -0
- package/dist/diagram.test.js.map +1 -0
- package/dist/duration.js +13 -5
- package/dist/duration.js.map +1 -1
- package/dist/effects.d.ts +6 -1
- package/dist/effects.js +97 -26
- package/dist/effects.js.map +1 -1
- package/dist/event-loop.d.ts +2 -3
- package/dist/event-loop.js +74 -44
- package/dist/event-loop.js.map +1 -1
- package/dist/format-parts.d.ts +21 -4
- package/dist/format-parts.js +40 -23
- package/dist/format-parts.js.map +1 -1
- package/dist/gateway-replay.e2e.test.js +4 -4
- package/dist/harness.e2e.test.js +2 -2
- package/dist/harness.e2e.test.js.map +1 -1
- package/dist/hello-thread.e2e.test.js +56 -5
- package/dist/hello-thread.e2e.test.js.map +1 -1
- package/dist/ingress.js +47 -12
- package/dist/ingress.js.map +1 -1
- package/dist/interaction-context.d.ts +4 -4
- package/dist/interaction-context.js +9 -7
- package/dist/interaction-context.js.map +1 -1
- package/dist/interrupt.e2e.test.js +8 -5
- package/dist/interrupt.e2e.test.js.map +1 -1
- package/dist/legacy.e2e.test.js +1 -1
- package/dist/lock-routes.d.ts +83 -31
- package/dist/lock-routes.js +68 -8
- package/dist/lock-routes.js.map +1 -1
- package/dist/lock-server.d.ts +3 -2
- package/dist/lock-server.js +23 -7
- package/dist/lock-server.js.map +1 -1
- package/dist/main.d.ts +6 -3
- package/dist/main.js +14 -4
- package/dist/main.js.map +1 -1
- package/dist/mainmast-idle.e2e.test.d.ts +1 -0
- package/dist/mainmast-idle.e2e.test.js +99 -0
- package/dist/mainmast-idle.e2e.test.js.map +1 -0
- package/dist/mainmast-queue.e2e.test.d.ts +1 -0
- package/dist/mainmast-queue.e2e.test.js +244 -0
- package/dist/mainmast-queue.e2e.test.js.map +1 -0
- package/dist/mainmast-warmup.e2e.test.d.ts +1 -0
- package/dist/mainmast-warmup.e2e.test.js +130 -0
- package/dist/mainmast-warmup.e2e.test.js.map +1 -0
- package/dist/mainmast.d.ts +6 -0
- package/dist/mainmast.e2e.test.d.ts +1 -0
- package/dist/mainmast.e2e.test.js +428 -0
- package/dist/mainmast.e2e.test.js.map +1 -0
- package/dist/mainmast.js +156 -0
- package/dist/mainmast.js.map +1 -0
- package/dist/markdown/markdown.test.js +14 -4
- package/dist/markdown/markdown.test.js.map +1 -1
- package/dist/markdown/render-markdown.js +9 -6
- package/dist/markdown/render-markdown.js.map +1 -1
- package/dist/onboarding-cli.e2e.test.js +7 -6
- package/dist/onboarding-cli.e2e.test.js.map +1 -1
- package/dist/onboarding.d.ts +3 -10
- package/dist/onboarding.e2e.test.js +8 -10
- package/dist/onboarding.e2e.test.js.map +1 -1
- package/dist/onboarding.js +14 -24
- package/dist/onboarding.js.map +1 -1
- package/dist/permissions.e2e.test.js +3 -3
- package/dist/plugin/coordinator.d.ts +4 -0
- package/dist/plugin/coordinator.js +389 -0
- package/dist/plugin/coordinator.js.map +1 -0
- package/dist/plugin/index.js +93 -61
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/mainmast.d.ts +4 -0
- package/dist/plugin/mainmast.js +379 -0
- package/dist/plugin/mainmast.js.map +1 -0
- package/dist/plugin.e2e.test.js +1 -1
- package/dist/preference-commands.e2e.test.js +103 -24
- package/dist/preference-commands.e2e.test.js.map +1 -1
- package/dist/project.d.ts +1 -1
- package/dist/prompt.d.ts +22 -9
- package/dist/prompt.js +64 -48
- package/dist/prompt.js.map +1 -1
- package/dist/questions.e2e.test.js +13 -13
- package/dist/queue-sources.e2e.test.d.ts +1 -0
- package/dist/queue-sources.e2e.test.js +168 -0
- package/dist/queue-sources.e2e.test.js.map +1 -0
- package/dist/queue.d.ts +9 -1
- package/dist/queue.e2e.test.js +48 -30
- package/dist/queue.e2e.test.js.map +1 -1
- package/dist/queue.js +97 -24
- package/dist/queue.js.map +1 -1
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +57 -0
- package/dist/queue.test.js.map +1 -0
- package/dist/remote-send.e2e.test.js +6 -6
- package/dist/remote-send.js +2 -1
- package/dist/remote-send.js.map +1 -1
- package/dist/renderer.e2e.test.js +7 -7
- package/dist/restart.e2e.test.js +2 -2
- package/dist/routines.e2e.test.js +2 -2
- package/dist/scheduler.d.ts +3 -3
- package/dist/scheduler.e2e.test.js +43 -20
- package/dist/scheduler.e2e.test.js.map +1 -1
- package/dist/scheduler.js +4 -2
- package/dist/scheduler.js.map +1 -1
- package/dist/schema-sql.d.ts +1 -1
- package/dist/schema-sql.js +1 -1
- package/dist/schema-sql.js.map +1 -1
- package/dist/schema.d.ts +152 -6
- package/dist/schema.js +12 -0
- package/dist/schema.js.map +1 -1
- package/dist/session-commands.e2e.test.js +36 -11
- package/dist/session-commands.e2e.test.js.map +1 -1
- package/dist/session-events.d.ts +18 -6
- package/dist/session-events.js +175 -27
- package/dist/session-events.js.map +1 -1
- package/dist/session-ops.e2e.test.js +63 -12
- package/dist/session-ops.e2e.test.js.map +1 -1
- package/dist/sessions.d.ts +44 -21
- package/dist/sessions.js +177 -65
- package/dist/sessions.js.map +1 -1
- package/dist/shell.e2e.test.js +4 -4
- package/dist/slash-commands.js +6 -4
- package/dist/slash-commands.js.map +1 -1
- package/dist/slash-commands.test.js +2 -0
- package/dist/slash-commands.test.js.map +1 -1
- package/dist/sleep.e2e.test.js +29 -14
- package/dist/sleep.e2e.test.js.map +1 -1
- package/dist/sleeps.d.ts +2 -1
- package/dist/sleeps.js +21 -3
- package/dist/sleeps.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.js +3 -2
- package/dist/store.js.map +1 -1
- package/dist/system-prompt.d.ts +5 -15
- package/dist/system-prompt.js +180 -131
- package/dist/system-prompt.js.map +1 -1
- package/dist/test/generate-log-plugin/index.d.ts +3 -0
- package/dist/test/generate-log-plugin/index.js +15 -0
- package/dist/test/generate-log-plugin/index.js.map +1 -0
- package/dist/test/harness.d.ts +20 -2
- package/dist/test/harness.js +51 -11
- package/dist/test/harness.js.map +1 -1
- package/dist/test/replay.js +11 -8
- package/dist/test/replay.js.map +1 -1
- package/dist/thread-reducer.d.ts +29 -6
- package/dist/thread-reducer.fixtures.test.js +58 -51
- package/dist/thread-reducer.fixtures.test.js.map +1 -1
- package/dist/thread-reducer.js +109 -50
- package/dist/thread-reducer.js.map +1 -1
- package/dist/thread-reducer.test.js +190 -48
- package/dist/thread-reducer.test.js.map +1 -1
- package/dist/tunnel.d.ts +9 -0
- package/dist/tunnel.js +117 -28
- package/dist/tunnel.js.map +1 -1
- package/dist/tunnel.test.js +81 -5
- package/dist/tunnel.test.js.map +1 -1
- package/dist/voice-attachments.e2e.test.js +108 -18
- package/dist/voice-attachments.e2e.test.js.map +1 -1
- package/dist/voice-call.e2e.test.js +9 -9
- package/dist/voice-call.js +6 -6
- package/dist/voice-call.js.map +1 -1
- package/dist/voice-openai.e2e.test.js +59 -23
- package/dist/voice-openai.e2e.test.js.map +1 -1
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +72 -24
- package/dist/voice.js.map +1 -1
- package/dist/working-directory.e2e.test.js +11 -11
- package/dist/working-directory.e2e.test.js.map +1 -1
- package/package.json +11 -5
- package/skills/kimaki-tunnel/SKILL.md +3 -2
- package/src/agent-ui.e2e.test.ts +27 -13
- package/src/agent-ui.ts +2 -1
- package/src/analytics.e2e.test.ts +1 -1
- package/src/autostart.ts +55 -10
- package/src/bot.ts +36 -19
- package/src/btw.e2e.test.ts +3 -3
- package/src/builtin-skills.test.ts +73 -1
- package/src/builtin-skills.ts +33 -0
- package/src/catalog-refresh.e2e.test.ts +2 -2
- package/src/cli/bot.ts +30 -19
- package/src/cli/discord.ts +30 -6
- package/src/cli/schedule.ts +23 -9
- package/src/cli/session.ts +35 -12
- package/src/cli/shared.ts +43 -8
- package/src/cli-discord.e2e.test.ts +13 -8
- package/src/cli-lock.e2e.test.ts +88 -20
- package/src/cli-read.e2e.test.ts +17 -8
- package/src/cli-sessions.e2e.test.ts +4 -3
- package/src/cli.ts +2 -0
- package/src/commands/preference-commands.ts +38 -4
- package/src/commands/session-commands.ts +12 -7
- package/src/commands/thread-commands.ts +92 -24
- package/src/commands/worktree-commands.ts +10 -9
- package/src/commands.e2e.test.ts +25 -21
- package/src/coordinator-agents.e2e.test.ts +145 -0
- package/src/coordinator-idle.e2e.test.ts +122 -0
- package/src/coordinator-queue.e2e.test.ts +279 -0
- package/src/coordinator-warmup.e2e.test.ts +158 -0
- package/src/coordinator.e2e.test.ts +545 -0
- package/src/coordinator.ts +168 -0
- package/src/db.test.ts +1 -0
- package/src/diagram.test.ts +56 -0
- package/src/diagram.ts +243 -0
- package/src/duration.ts +13 -5
- package/src/effects.ts +99 -27
- package/src/event-loop.ts +71 -45
- package/src/format-parts.ts +53 -22
- package/src/gateway-replay.e2e.test.ts +4 -4
- package/src/harness.e2e.test.ts +2 -2
- package/src/hello-thread.e2e.test.ts +59 -5
- package/src/ingress.ts +44 -11
- package/src/interaction-context.ts +12 -11
- package/src/interrupt.e2e.test.ts +8 -5
- package/src/legacy.e2e.test.ts +1 -1
- package/src/lock-routes.ts +62 -8
- package/src/lock-server.ts +23 -7
- package/src/main.ts +26 -10
- package/src/markdown/markdown.test.ts +15 -4
- package/src/markdown/render-markdown.ts +9 -6
- package/src/onboarding-cli.e2e.test.ts +7 -6
- package/src/onboarding.e2e.test.ts +8 -10
- package/src/onboarding.ts +13 -29
- package/src/permissions.e2e.test.ts +3 -3
- package/src/plugin/coordinator.ts +401 -0
- package/src/plugin/index.ts +86 -59
- package/src/plugin.e2e.test.ts +1 -1
- package/src/preference-commands.e2e.test.ts +103 -23
- package/src/prompt.ts +87 -43
- package/src/questions.e2e.test.ts +13 -13
- package/src/queue-sources.e2e.test.ts +197 -0
- package/src/queue.e2e.test.ts +48 -30
- package/src/queue.test.ts +59 -0
- package/src/queue.ts +104 -24
- package/src/remote-send.e2e.test.ts +6 -6
- package/src/remote-send.ts +2 -1
- package/src/renderer.e2e.test.ts +7 -7
- package/src/restart.e2e.test.ts +2 -2
- package/src/routines.e2e.test.ts +2 -2
- package/src/scheduler.e2e.test.ts +45 -19
- package/src/scheduler.ts +4 -2
- package/src/schema-sql.ts +1 -1
- package/src/schema.ts +13 -0
- package/src/session-commands.e2e.test.ts +38 -11
- package/src/session-events.ts +168 -25
- package/src/session-ops.e2e.test.ts +64 -11
- package/src/sessions.ts +217 -67
- package/src/shell.e2e.test.ts +4 -4
- package/src/slash-commands.test.ts +2 -0
- package/src/slash-commands.ts +6 -4
- package/src/sleep.e2e.test.ts +30 -13
- package/src/sleeps.ts +20 -3
- package/src/store.ts +5 -2
- package/src/system-prompt.ts +190 -134
- package/src/test/generate-log-plugin/index.ts +16 -0
- package/src/test/harness.ts +77 -17
- package/src/test/replay.ts +12 -7
- package/src/thread-reducer.fixtures.test.ts +58 -51
- package/src/thread-reducer.test.ts +199 -49
- package/src/thread-reducer.ts +135 -66
- package/src/tunnel.test.ts +81 -5
- package/src/tunnel.ts +124 -25
- package/src/voice-attachments.e2e.test.ts +110 -18
- package/src/voice-call.e2e.test.ts +9 -9
- package/src/voice-call.ts +6 -6
- package/src/voice-openai.e2e.test.ts +64 -24
- package/src/voice.ts +93 -26
- package/src/working-directory.e2e.test.ts +11 -11
- package/src/message-tool.e2e.test.ts +0 -45
package/src/queue.ts
CHANGED
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
//
|
|
5
5
|
// inbox.enqueued (queue, while busy) ─▶ "Queued at position N" ack
|
|
6
6
|
// inbox.enqueued (steer, echo) ─▶ the echo line (wakes, scheduled runs, TUI prompts)
|
|
7
|
-
// inbox.delivered
|
|
7
|
+
// inbox.delivered (or promoted) ─▶ "Queued message sent" reply to the user message, ack says "Queued"
|
|
8
8
|
// inbox.cancelled ─▶ ack says "Removed from queue"
|
|
9
9
|
//
|
|
10
|
+
// Every acked item gets that reply when it starts, in threads and coordinator
|
|
11
|
+
// channels alike. The prompt text is never posted again. Prompts without a Discord
|
|
12
|
+
// message (CLI, routines, TUI) have nothing for the ack to reply to: the ack
|
|
13
|
+
// shows the full prompt, and the "Queued message sent" reply goes to the ack.
|
|
14
|
+
//
|
|
10
15
|
// A queued prompt's inbox ID is msg_discord_<messageId> (prompt.ts), so a
|
|
11
16
|
// message delete or an edit maps to the item by ID alone. Items without a
|
|
12
17
|
// Discord source message (/queue, CLI, commands) are removed with /clear-queue.
|
|
@@ -18,10 +23,10 @@ import type { Draft } from 'immer'
|
|
|
18
23
|
import type { Bot } from './bot.ts'
|
|
19
24
|
import { asSubtext, textOnly } from './format-parts.ts'
|
|
20
25
|
import { stripTurnContext } from './system-prompt.ts'
|
|
26
|
+
import { markdownPayloads, type DiscordPayload } from './markdown/components.ts'
|
|
27
|
+
import { DISCORD_TEXT_LIMIT } from './markdown/render-markdown.ts'
|
|
21
28
|
import type { Emit, ThreadView } from './thread-reducer.ts'
|
|
22
29
|
|
|
23
|
-
const ECHO_LIMIT = 1_900
|
|
24
|
-
|
|
25
30
|
// A root inbox user item not delivered yet, in inbox order.
|
|
26
31
|
export type PendingInput = {
|
|
27
32
|
inboxID: string
|
|
@@ -31,6 +36,10 @@ export type PendingInput = {
|
|
|
31
36
|
username: string | null
|
|
32
37
|
// Discord message the prompt came from (the ack replies to it).
|
|
33
38
|
messageId: string | null
|
|
39
|
+
// The first answer text replies to this message. Only in a coordinator channel:
|
|
40
|
+
// a thread holds one conversation, so turns need no extra delimiter there.
|
|
41
|
+
// Not for acked items: their "Queued message sent" reply marks the start.
|
|
42
|
+
replyTo: string | null
|
|
34
43
|
// An ack was posted; only acked items get an echo when they start.
|
|
35
44
|
acked: boolean
|
|
36
45
|
// Line from the prompt metadata (scheduled runs) or for prompts sent outside Discord; shown on delivery even without an ack.
|
|
@@ -45,36 +54,102 @@ function ackKey(inboxID: string): string {
|
|
|
45
54
|
return `queue:${inboxID}`
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
|
|
57
|
+
// CLI and scheduled prompts carry a UUID, not a Discord message to reply to.
|
|
58
|
+
export function discordMessageId(messageId: string): string | null {
|
|
59
|
+
return /^\d+$/.test(messageId) ? messageId : null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function inputFields({ payload, channelId }: { payload: { text: string; metadata?: { readonly [key: string]: JsonValue } }; channelId: string }) {
|
|
49
63
|
const text = stripTurnContext(payload.text)
|
|
50
64
|
const discord = payload.metadata?.['discord']
|
|
51
65
|
// Sent outside Discord (TUI, other clients): echo it like V1 external sync did.
|
|
52
66
|
if (!discord || typeof discord !== 'object' || Array.isArray(discord)) {
|
|
53
|
-
return { external: true, text, username: null, messageId: null, echo: text.trim() ? formatEcho({ username: 'user', text }) : null }
|
|
67
|
+
return { external: true, text, username: null, messageId: null, replyTo: null, echo: text.trim() ? formatEcho({ username: 'user', text }) : null }
|
|
54
68
|
}
|
|
55
69
|
const username = discord['username']
|
|
56
|
-
const
|
|
70
|
+
const raw = discord['messageId']
|
|
57
71
|
const echo = discord['echo']
|
|
72
|
+
const messageId = typeof raw === 'string' ? discordMessageId(raw) : null
|
|
58
73
|
return {
|
|
59
74
|
external: false,
|
|
60
75
|
text,
|
|
61
76
|
username: typeof username === 'string' ? username : null,
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
messageId,
|
|
78
|
+
// A coordinator prompt's conversation is the project channel itself.
|
|
79
|
+
replyTo: discord['threadId'] === channelId ? messageId : null,
|
|
64
80
|
// Shown when a prompt without a Discord message is taken (wake, scheduled run).
|
|
65
81
|
echo: typeof echo === 'string' ? echo : null,
|
|
66
82
|
}
|
|
67
83
|
}
|
|
68
84
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
function echoLine({ username, text }: { username: string | null; text: string }): string {
|
|
86
|
+
// Block markdown only renders at the start of a line, so it goes below the prefix.
|
|
87
|
+
const separator = BLOCK_MARKDOWN_START.test(text) ? '\n' : ' '
|
|
88
|
+
return `» **${username ?? 'queued'}:**${separator}${text}`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Headings, quotes, lists, code fences, rules, tables and subtext.
|
|
92
|
+
const BLOCK_MARKDOWN_START = /^ {0,3}(#{1,6}(\s|$)|-#\s|>|[-*+]\s|\d{1,9}[.)]\s|```|~~~|-{3,}\s*$|\|)/
|
|
93
|
+
const CODE_FENCE = /^ {0,3}(```|~~~)/
|
|
94
|
+
|
|
95
|
+
// Every prompt the bot posts (CLI, routines, TUI, voice, slash commands) is
|
|
96
|
+
// subtext, so it reads apart from model output. A long one becomes several messages (effects.ts).
|
|
97
|
+
export function formatEcho(input: { username: string | null; text: string }): string {
|
|
98
|
+
return subtext(echoLine(input))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A long echo (formatEcho) as several messages, split like model markdown.
|
|
102
|
+
// Each piece gets its `-# ` again: a line cut at a word break would lose it.
|
|
103
|
+
export function echoPayloads(echo: string): DiscordPayload[] {
|
|
104
|
+
return markdownPayloads(echo).map((payload) => ('content' in payload ? { content: subtext(payload.content.trim()) } : payload))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// A slash command reply stands in for the user message: one Discord message, long text is cut.
|
|
108
|
+
export function formatEchoReply(input: { username: string | null; text: string }): string {
|
|
109
|
+
return cut(formatEcho(input), DISCORD_TEXT_LIMIT)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Every non-empty line gets `-# `. Subtext only renders inline markdown: code
|
|
113
|
+
// blocks stay as they are, headings become bold, other block markers show as text.
|
|
114
|
+
function subtext(text: string): string {
|
|
115
|
+
let fenced = false
|
|
116
|
+
return text
|
|
117
|
+
.split('\n')
|
|
118
|
+
.flatMap((line) => {
|
|
119
|
+
if (CODE_FENCE.test(line)) fenced = !fenced
|
|
120
|
+
else if (!fenced && line.trim() === '') return []
|
|
121
|
+
else if (!fenced && !line.startsWith('-# ')) return [asSubtext(line.replace(HEADING, '**$1**'))]
|
|
122
|
+
return [line]
|
|
123
|
+
})
|
|
124
|
+
.join('\n')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const HEADING = /^ {0,3}#{1,6}\s+(.+?)\s*$/
|
|
128
|
+
|
|
129
|
+
function cut(text: string, limit: number): string {
|
|
130
|
+
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The ack of a prompt with a Discord message replies to it. Without one (CLI,
|
|
134
|
+
// routines, TUI) the ack itself shows the prompt, and its status line below.
|
|
135
|
+
function ackContent(item: PendingInput, status: string | null): string {
|
|
136
|
+
if (item.messageId) return asSubtext(status ?? '')
|
|
137
|
+
const line = status ? `\n${asSubtext(status)}` : ''
|
|
138
|
+
return `${cut(subtext(item.echo ?? echoLine(item)), DISCORD_TEXT_LIMIT - line.length)}${line}`
|
|
72
139
|
}
|
|
73
140
|
|
|
74
141
|
type Slice = { draft: Draft<ThreadView>; emit: Emit }
|
|
75
142
|
|
|
76
|
-
function closeAck({ emit }: Slice, item: PendingInput,
|
|
77
|
-
if (item.acked) emit({ type: 'edit', key: ackKey(item.inboxID), messages: [textOnly(
|
|
143
|
+
function closeAck({ emit }: Slice, item: PendingInput, status: string) {
|
|
144
|
+
if (item.acked) emit({ type: 'edit', key: ackKey(item.inboxID), messages: [textOnly(ackContent(item, status))] })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// An acked item starts running. The user message (or the ack without one)
|
|
148
|
+
// already shows the prompt: a reply to it marks where it runs, instead of a
|
|
149
|
+
// second copy. Also in a coordinator, where the answer may post no text (IDLE).
|
|
150
|
+
function markStarted({ emit }: Slice, item: PendingInput) {
|
|
151
|
+
const ack = textOnly(ackContent(item, item.messageId ? 'Queued' : null))
|
|
152
|
+
emit({ type: 'edit', key: ackKey(item.inboxID), messages: [ack], reply: asSubtext('Queued message sent'), replyTo: item.messageId })
|
|
78
153
|
}
|
|
79
154
|
|
|
80
155
|
// Removes the item from the inbox; returns it when it was still queued.
|
|
@@ -91,32 +166,34 @@ export function enqueueInput(
|
|
|
91
166
|
const { draft, emit, data, busy } = slice
|
|
92
167
|
const { item, inboxID } = data
|
|
93
168
|
if (item.type !== 'user' || draft.inbox.some((candidate) => candidate.inboxID === inboxID)) return
|
|
94
|
-
const { external, ...fields } = inputFields(item.payload)
|
|
169
|
+
const { external, ...fields } = inputFields({ payload: item.payload, channelId: draft.channelId })
|
|
95
170
|
// The expanded prompt of a command Kimaki ran: the queue echo shows the typed command, never the template.
|
|
96
171
|
const command = external ? draft.commandPending : null
|
|
97
172
|
if (external) draft.commandPending = null
|
|
98
|
-
const meta = command ? { ...fields, ...command
|
|
173
|
+
const meta = command ? { ...fields, ...command } : fields
|
|
99
174
|
// Idle with nothing pending: OpenCode runs it at once, like a normal message.
|
|
100
175
|
const acked = item.delivery === 'queue' && (busy || draft.inbox.length > 0)
|
|
101
|
-
|
|
176
|
+
const pending: PendingInput = { inboxID, delivery: item.delivery, ...meta, acked }
|
|
177
|
+
draft.inbox.push(pending)
|
|
102
178
|
if (item.delivery !== 'queue') {
|
|
103
179
|
if (!meta.echo) return
|
|
104
|
-
emit({ type: 'send', text: meta.echo })
|
|
180
|
+
emit({ type: 'send', text: meta.echo, echo: true })
|
|
105
181
|
draft.lastKind = null
|
|
106
182
|
return
|
|
107
183
|
}
|
|
108
184
|
if (!acked) return
|
|
109
185
|
const position = queuedItems(draft).length
|
|
110
|
-
const
|
|
111
|
-
|
|
186
|
+
const remove = meta.messageId ? 'Delete the original message to remove it, or use' : 'Remove it with'
|
|
187
|
+
const ack = textOnly(ackContent(pending, `Queued at position ${position}. ${remove} /clear-queue position:${position}`))
|
|
188
|
+
emit({ type: 'show', key: ackKey(inboxID), messages: [ack], replyTo: meta.messageId, notify: false, echo: true })
|
|
112
189
|
}
|
|
113
190
|
|
|
114
191
|
export function deliverQueued(slice: Slice & { inboxID: string }) {
|
|
115
192
|
const item = takeQueued(slice, slice.inboxID)
|
|
116
193
|
if (!item || (!item.acked && !item.echo)) return
|
|
117
|
-
closeAck(slice, item, 'Queued message sent')
|
|
118
|
-
slice.emit({ type: 'send', text: item.echo ?? formatEcho(item) })
|
|
119
194
|
slice.draft.lastKind = null
|
|
195
|
+
if (item.acked) return markStarted(slice, item)
|
|
196
|
+
slice.emit({ type: 'send', text: item.echo ?? formatEcho(item), echo: true })
|
|
120
197
|
}
|
|
121
198
|
|
|
122
199
|
export function cancelQueued(slice: Slice & { inboxID: string }) {
|
|
@@ -128,7 +205,7 @@ export function cancelQueued(slice: Slice & { inboxID: string }) {
|
|
|
128
205
|
export function promoteQueued(slice: Slice & { inboxID: string }) {
|
|
129
206
|
const item = slice.draft.inbox.find((candidate) => candidate.inboxID === slice.inboxID)
|
|
130
207
|
if (item?.delivery !== 'queue') return
|
|
131
|
-
|
|
208
|
+
if (item.acked) markStarted(slice, { ...item })
|
|
132
209
|
item.delivery = 'steer'
|
|
133
210
|
}
|
|
134
211
|
|
|
@@ -146,8 +223,11 @@ export function hydrateQueue(slice: Slice & { inbox: readonly SessionInboxInfo[]
|
|
|
146
223
|
draft.inbox = users.map((item): PendingInput => {
|
|
147
224
|
const existing = known.get(item.id)
|
|
148
225
|
if (existing?.delivery === item.delivery) return existing
|
|
149
|
-
if (existing?.delivery === 'queue' && item.delivery === 'steer')
|
|
150
|
-
|
|
226
|
+
if (existing?.delivery === 'queue' && item.delivery === 'steer') {
|
|
227
|
+
if (existing.acked) markStarted(slice, existing)
|
|
228
|
+
return { ...existing, delivery: 'steer' }
|
|
229
|
+
}
|
|
230
|
+
const { external: _external, ...fields } = inputFields({ payload: item.payload, channelId: draft.channelId })
|
|
151
231
|
return { inboxID: item.id, delivery: item.delivery, ...fields, acked: item.delivery === 'queue' }
|
|
152
232
|
})
|
|
153
233
|
}
|
|
@@ -38,17 +38,17 @@ test('CLI remote envelope runs only on the owning bot and returns its IDs', asyn
|
|
|
38
38
|
await waitForFooter({ discord: twin.discord, threadId: ids.threadId, count: 2 })
|
|
39
39
|
expect((await twin.discord.thread(ids.threadId).text()).replaceAll(ids.threadId, 'THREAD')).toMatchInlineSnapshot(`
|
|
40
40
|
"--- from: assistant (TestBot)
|
|
41
|
-
» **CLI:** Remote first
|
|
42
|
-
Files: remote.txt
|
|
41
|
+
-# » **CLI:** Remote first
|
|
42
|
+
-# Files: remote.txt
|
|
43
43
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
44
44
|
ok
|
|
45
|
-
-# *
|
|
45
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*
|
|
46
46
|
Remote queued. queue
|
|
47
47
|
[embed]
|
|
48
48
|
Delivered to <#THREAD>
|
|
49
49
|
[embed]
|
|
50
50
|
ok
|
|
51
|
-
-# *
|
|
51
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
52
52
|
`)
|
|
53
53
|
expect(a.store.getState().roots[ids.threadId]).toBeUndefined()
|
|
54
54
|
expect(b.store.getState().roots[ids.threadId]).toBe(ids.sessionId)
|
|
@@ -67,10 +67,10 @@ test('CI send with only KIMAKI_BOT_TOKEN and no local bot reaches the owning bot
|
|
|
67
67
|
await waitForFooter({ discord: twin.discord, threadId: ids.threadId })
|
|
68
68
|
expect(await twin.discord.thread(ids.threadId).text()).toMatchInlineSnapshot(`
|
|
69
69
|
"--- from: assistant (TestBot)
|
|
70
|
-
» **CLI:** From CI
|
|
70
|
+
-# » **CLI:** From CI
|
|
71
71
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
72
72
|
ok
|
|
73
|
-
-# *
|
|
73
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
74
74
|
`)
|
|
75
75
|
expect(b.store.getState().roots[ids.threadId]).toBe(ids.sessionId)
|
|
76
76
|
expect(fs.readdirSync(ciDataDir)).toEqual([])
|
package/src/remote-send.ts
CHANGED
|
@@ -36,7 +36,8 @@ export function promptFilePath(file: { uri: string }): string {
|
|
|
36
36
|
// file path; the prompt file goes first, because the receiver reads the first attachment.
|
|
37
37
|
export function remoteEnvelope(input: SendInput) {
|
|
38
38
|
const requestId = crypto.randomBytes(8).toString('hex')
|
|
39
|
-
|
|
39
|
+
// A session of this machine means nothing to the receiving bot.
|
|
40
|
+
const { files, prompt, senderSessionId: _sender, ...options } = input
|
|
40
41
|
// A prompt over the message limit travels as an attachment; the content keeps a preview.
|
|
41
42
|
const long = prompt.length > 2000
|
|
42
43
|
const footer = `${REMOTE_SEND_PREFIX}${JSON.stringify({ requestId, options, ...(long && { promptFile: REMOTE_PROMPT_FILE }) })}`
|
package/src/renderer.e2e.test.ts
CHANGED
|
@@ -166,7 +166,7 @@ test('shell and edit lines are shown, read is hidden at the default verbosity',
|
|
|
166
166
|
-# ◼︎ edit *notes.md* (+1-0)
|
|
167
167
|
|
|
168
168
|
Edited notes.md.
|
|
169
|
-
-# *
|
|
169
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
170
170
|
`)
|
|
171
171
|
})
|
|
172
172
|
|
|
@@ -182,7 +182,7 @@ test('Code Mode execute shows its description, then one line per inner tool call
|
|
|
182
182
|
-# ┣ execute.opencode.list\\_mcp\\_resources
|
|
183
183
|
|
|
184
184
|
Found the models.
|
|
185
|
-
-# *
|
|
185
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
186
186
|
`)
|
|
187
187
|
})
|
|
188
188
|
|
|
@@ -199,7 +199,7 @@ test('Code Mode execute shows failed inner calls and the code error', async () =
|
|
|
199
199
|
-# ⨯ execute _Error: broken on purpose_
|
|
200
200
|
|
|
201
201
|
The code failed.
|
|
202
|
-
-# *
|
|
202
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
203
203
|
`)
|
|
204
204
|
})
|
|
205
205
|
|
|
@@ -252,7 +252,7 @@ test('table and callout render as components, long code splits into valid messag
|
|
|
252
252
|
{
|
|
253
253
|
"components": false,
|
|
254
254
|
"fences": 0,
|
|
255
|
-
"length":
|
|
255
|
+
"length": 39,
|
|
256
256
|
},
|
|
257
257
|
]
|
|
258
258
|
`)
|
|
@@ -356,7 +356,7 @@ test('table and callout render as components, long code splits into valid messag
|
|
|
356
356
|
const value78 = compute(78) // line 78
|
|
357
357
|
const value79 = compute(79) // line 79
|
|
358
358
|
\`\`\`
|
|
359
|
-
-# *
|
|
359
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
360
360
|
`)
|
|
361
361
|
})
|
|
362
362
|
|
|
@@ -371,7 +371,7 @@ test('foreground subagent: child tool lines labelled with the agent, one footer'
|
|
|
371
371
|
-# ┣ general ⋅ shell _ls \\*.md_
|
|
372
372
|
|
|
373
373
|
The child found notes.md.
|
|
374
|
-
-# *
|
|
374
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
375
375
|
`)
|
|
376
376
|
})
|
|
377
377
|
|
|
@@ -385,6 +385,6 @@ test('channel verbosity text shows only text and edit lines', async () => {
|
|
|
385
385
|
-# ◼︎ edit *notes.md* (+1-0)
|
|
386
386
|
|
|
387
387
|
Edited notes.md.
|
|
388
|
-
-# *
|
|
388
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
389
389
|
`)
|
|
390
390
|
})
|
package/src/restart.e2e.test.ts
CHANGED
|
@@ -62,12 +62,12 @@ test('restarted bot continues the session of an existing thread', async () => {
|
|
|
62
62
|
--- from: assistant (TestBot)
|
|
63
63
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
64
64
|
ok
|
|
65
|
-
-# *
|
|
65
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*
|
|
66
66
|
--- from: user (tommy)
|
|
67
67
|
second message after restart
|
|
68
68
|
--- from: assistant (TestBot)
|
|
69
69
|
ok
|
|
70
|
-
-# *
|
|
70
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
71
71
|
`)
|
|
72
72
|
const rows = await bot.db.query.thread_sessions.findMany()
|
|
73
73
|
expect(rows.map((row) => row.session_id)).toEqual([binding?.session_id])
|
package/src/routines.e2e.test.ts
CHANGED
|
@@ -92,10 +92,10 @@ test('a routine row written by V1 runs unchanged', async () => {
|
|
|
92
92
|
await waitForFooter({ discord: twin.discord, threadId: threadId! })
|
|
93
93
|
expect(await twin.discord.thread(threadId!).text()).toMatchInlineSnapshot(`
|
|
94
94
|
"--- from: assistant (TestBot)
|
|
95
|
-
» **routine #3:** V1 weekly check
|
|
95
|
+
-# » **routine #3:** V1 weekly check
|
|
96
96
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
97
97
|
ok
|
|
98
|
-
-# *
|
|
98
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
99
99
|
`)
|
|
100
100
|
expect((await listTasks()).find((task) => task.id === row!.id)).toMatchObject({ status: 'planned', nextRunAt: '2030-01-14T09:00:00.000Z' })
|
|
101
101
|
// V1 history: finished rows only show with the V1 `task list --all`.
|
|
@@ -12,10 +12,13 @@ import {
|
|
|
12
12
|
manualClock,
|
|
13
13
|
schedulingKit,
|
|
14
14
|
seedProjectChannel,
|
|
15
|
+
slowTextMatcher,
|
|
15
16
|
startOpencodeTestServer,
|
|
16
17
|
startTestBot,
|
|
17
18
|
startTwin,
|
|
18
19
|
tempDataDir,
|
|
20
|
+
textParts,
|
|
21
|
+
waitForBotMessageContaining,
|
|
19
22
|
waitForFooter,
|
|
20
23
|
warmUp,
|
|
21
24
|
type OpencodeTestServer,
|
|
@@ -30,7 +33,10 @@ let bot: BotHandle
|
|
|
30
33
|
const { cli, schedule, tick, listTasks, request } = schedulingKit({ suite: () => ({ bot, twin, server }), dataDir, clock })
|
|
31
34
|
|
|
32
35
|
beforeAll(async () => {
|
|
33
|
-
;[server, twin] = await Promise.all([startOpencodeTestServer(
|
|
36
|
+
;[server, twin] = await Promise.all([startOpencodeTestServer({ matchers: [
|
|
37
|
+
slowTextMatcher({ marker: 'slow-marker', text: 'slow-done', delayMs: 800 }),
|
|
38
|
+
{ id: 'report', priority: 50, when: { latestUserTextIncludes: 'Report when done' }, then: { parts: textParts('report ok') } },
|
|
39
|
+
] }), startTwin()])
|
|
34
40
|
await seedProjectChannel({ dataDir, channelId: twin.channelId, guildId: twin.discord.guildId, directory: server.projectDirectory })
|
|
35
41
|
bot = await startTestBot({ dataDir, twin, server, clock, schedulerIntervalMs: null })
|
|
36
42
|
await warmUp({ server })
|
|
@@ -51,10 +57,10 @@ test('a one-shot routine starts a thread at its time, then is gone', async () =>
|
|
|
51
57
|
await waitForFooter({ discord: twin.discord, threadId: threadId! })
|
|
52
58
|
expect(await twin.discord.thread(threadId!).text()).toMatchInlineSnapshot(`
|
|
53
59
|
"--- from: assistant (TestBot)
|
|
54
|
-
» **routine #1:** Check the deploy once
|
|
60
|
+
-# » **routine #1:** Check the deploy once
|
|
55
61
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
56
62
|
ok
|
|
57
|
-
-# *
|
|
63
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
58
64
|
`)
|
|
59
65
|
const thread = (await twin.discord.channel(twin.channelId).getThreads()).find((candidate) => candidate.id === threadId)
|
|
60
66
|
expect(thread?.name).toBe('Deploy check')
|
|
@@ -93,10 +99,10 @@ test('a cron routine fires once per occurrence, in a new thread each time', asyn
|
|
|
93
99
|
expect([first.length, second.length]).toEqual([1, 1])
|
|
94
100
|
expect(await twin.discord.thread(second[0]!).text()).toMatchInlineSnapshot(`
|
|
95
101
|
"--- from: assistant (TestBot)
|
|
96
|
-
» **routine #2:** Hourly report
|
|
102
|
+
-# » **routine #2:** Hourly report
|
|
97
103
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
98
104
|
ok
|
|
99
|
-
-# *
|
|
105
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
100
106
|
`)
|
|
101
107
|
expect((await listTasks()).find((row) => row.id === task.routineId)).toMatchObject({ status: 'planned', nextRunAt: '2030-01-01T13:00:00.000Z' })
|
|
102
108
|
await request('task.delete', { id: task.routineId })
|
|
@@ -114,16 +120,16 @@ test('a routine for an existing thread prompts that thread, with or without . qu
|
|
|
114
120
|
await waitForFooter({ discord: twin.discord, threadId: started.threadId, count: 3 })
|
|
115
121
|
expect(await twin.discord.thread(started.threadId).text()).toMatchInlineSnapshot(`
|
|
116
122
|
"--- from: assistant (TestBot)
|
|
117
|
-
» **CLI:** Thread for a reminder
|
|
123
|
+
-# » **CLI:** Thread for a reminder
|
|
118
124
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
119
125
|
ok
|
|
120
|
-
-# *
|
|
121
|
-
» **routine #3:** Reminder: check the logs
|
|
126
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*
|
|
127
|
+
-# » **routine #3:** Reminder: check the logs
|
|
122
128
|
ok
|
|
123
|
-
-# *
|
|
124
|
-
» **routine #4:** Then summarize them
|
|
129
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*
|
|
130
|
+
-# » **routine #4:** Then summarize them
|
|
125
131
|
ok
|
|
126
|
-
-# *
|
|
132
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
127
133
|
`)
|
|
128
134
|
})
|
|
129
135
|
|
|
@@ -136,14 +142,12 @@ test('--pre-run: a failing command skips the run, output is appended to the prom
|
|
|
136
142
|
await waitForFooter({ discord: twin.discord, threadId: created[0]! })
|
|
137
143
|
expect(await twin.discord.thread(created[0]!).text()).toMatchInlineSnapshot(`
|
|
138
144
|
"--- from: assistant (TestBot)
|
|
139
|
-
» **routine #6:** Summarize the new issues
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
issue 42 is new
|
|
145
|
+
-# » **routine #6:** Summarize the new issues
|
|
146
|
+
-# **Pre-run command output**
|
|
147
|
+
-# issue 42 is new
|
|
144
148
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
145
149
|
ok
|
|
146
|
-
-# *
|
|
150
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
147
151
|
`)
|
|
148
152
|
// A skipped one-shot is done too, like V1.
|
|
149
153
|
expect((await listTasks()).filter((row) => row.id === skipped.routineId || row.id === ran.routineId)).toEqual([])
|
|
@@ -162,14 +166,14 @@ test('recurring worktree routines get a fresh checkout on every run', async () =
|
|
|
162
166
|
const normalize = (text: string) => text.replace(/report-[a-f0-9]{8}/g, 'report-ID')
|
|
163
167
|
expect(normalize(await twin.discord.thread(first[0]!).text())).toMatchInlineSnapshot(`
|
|
164
168
|
"--- from: assistant (TestBot)
|
|
165
|
-
» **routine #7:** Isolated recurring report
|
|
169
|
+
-# » **routine #7:** Isolated recurring report
|
|
166
170
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
167
171
|
ok
|
|
168
172
|
-# *report-ID ⋅ opencode/kimaki-report-ID ⋅ Ns ⋅ deterministic-v2*"
|
|
169
173
|
`)
|
|
170
174
|
expect(normalize(await twin.discord.thread(second[0]!).text())).toMatchInlineSnapshot(`
|
|
171
175
|
"--- from: assistant (TestBot)
|
|
172
|
-
» **routine #7:** Isolated recurring report
|
|
176
|
+
-# » **routine #7:** Isolated recurring report
|
|
173
177
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
174
178
|
ok
|
|
175
179
|
-# *report-ID ⋅ opencode/kimaki-report-ID ⋅ Ns ⋅ deterministic-v2*"
|
|
@@ -180,3 +184,25 @@ test('recurring worktree routines get a fresh checkout on every run', async () =
|
|
|
180
184
|
expect(directories.every((directory) => directory !== server.projectDirectory)).toBe(true)
|
|
181
185
|
await request('task.delete', { id: task.routineId })
|
|
182
186
|
})
|
|
187
|
+
|
|
188
|
+
// Without --allow-concurrency a thread routine waits until the thread is idle, then runs at once.
|
|
189
|
+
test('a . queue routine with --allow-concurrency that fires while its thread is busy waits in the queue, then gets its reply', async () => {
|
|
190
|
+
const started = await send(bot, { channelId: twin.channelId, prompt: 'Long work slow-marker' })
|
|
191
|
+
if (started instanceof Error) throw started
|
|
192
|
+
await waitForBotMessageContaining({ discord: twin.discord, threadId: started.threadId, text: '*using ' })
|
|
193
|
+
await request('send', { threadId: started.threadId, prompt: 'Report when done. queue', sendAt: '2030-01-01T19:00:00Z', allowConcurrency: true })
|
|
194
|
+
expect(await tick('2030-01-01T19:00:00Z')).toEqual([])
|
|
195
|
+
await waitForFooter({ discord: twin.discord, threadId: started.threadId, count: 2 })
|
|
196
|
+
expect(await twin.discord.thread(started.threadId).text({ showReplies: true })).toMatchInlineSnapshot(`
|
|
197
|
+
"--- from: assistant (TestBot)
|
|
198
|
+
-# » **CLI:** Long work slow-marker
|
|
199
|
+
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
200
|
+
-# » **routine #8:** Report when done
|
|
201
|
+
slow-done
|
|
202
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
203
|
+
[reply to TestBot: -# » **routine #8:** Report when done]
|
|
204
|
+
-# Queued message sent
|
|
205
|
+
report ok
|
|
206
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
207
|
+
`)
|
|
208
|
+
})
|
package/src/scheduler.ts
CHANGED
|
@@ -423,8 +423,10 @@ export async function createTask(bot: Bot, { send: input, options }: { send: Sen
|
|
|
423
423
|
if (threadId) {
|
|
424
424
|
const thread = await bot.discord.channels.fetch(threadId).catch((cause) => new DiscordError({ operation: `fetch thread ${threadId}`, cause }))
|
|
425
425
|
if (thread instanceof Error) return thread
|
|
426
|
-
|
|
427
|
-
|
|
426
|
+
// A coordinator channel is its own project channel.
|
|
427
|
+
const channelId = thread?.isThread() ? thread.parentId : thread?.id
|
|
428
|
+
if (!channelId) return null
|
|
429
|
+
return projectOf(bot, channelId)
|
|
428
430
|
}
|
|
429
431
|
return bot.db.query.channel_directories
|
|
430
432
|
.findFirst({ where: input.channelId ? { channel_id: input.channelId } : { directory: path.resolve(input.project!) } })
|
package/src/schema-sql.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by pnpm generate from src/schema.ts. Do not edit.
|
|
2
2
|
|
|
3
|
-
export const SCHEMA_SQL = "CREATE TABLE IF NOT EXISTS `bot_api_keys` (\n\t`app_id` text PRIMARY KEY,\n\t`gemini_api_key` text,\n\t`openai_api_key` text,\n\t`xai_api_key` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_bot_api_keys_app_id_bot_tokens_app_id_fk` FOREIGN KEY (`app_id`) REFERENCES `bot_tokens`(`app_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `bot_settings` (\n\t`app_id` text PRIMARY KEY,\n\t`voice_call_voice` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_bot_settings_app_id_bot_tokens_app_id_fk` FOREIGN KEY (`app_id`) REFERENCES `bot_tokens`(`app_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `bot_tokens` (\n\t`app_id` text PRIMARY KEY,\n\t`token` text NOT NULL,\n\t`bot_mode` text DEFAULT 'self_hosted' NOT NULL,\n\t`client_id` text,\n\t`client_secret` text,\n\t`proxy_url` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`last_used_at` datetime\n);\n\nCREATE TABLE IF NOT EXISTS `channel_agents` (\n\t`channel_id` text PRIMARY KEY,\n\t`agent_name` text NOT NULL,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_channel_agents_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `channel_directories` (\n\t`channel_id` text PRIMARY KEY,\n\t`directory` text NOT NULL,\n\t`channel_type` text NOT NULL,\n\t`guild_id` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE IF NOT EXISTS `channel_mention_mode` (\n\t`channel_id` text PRIMARY KEY,\n\t`enabled` integer DEFAULT 0 NOT NULL,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_channel_mention_mode_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `channel_models` (\n\t`channel_id` text PRIMARY KEY,\n\t`model_id` text NOT NULL,\n\t`variant` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_channel_models_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `channel_verbosity` (\n\t`channel_id` text PRIMARY KEY,\n\t`verbosity` text DEFAULT 'tools_and_text' NOT NULL,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_channel_verbosity_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `channel_worktrees` (\n\t`channel_id` text PRIMARY KEY,\n\t`enabled` integer DEFAULT 0 NOT NULL,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_channel_worktrees_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `global_models` (\n\t`app_id` text PRIMARY KEY,\n\t`model_id` text NOT NULL,\n\t`variant` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_global_models_app_id_bot_tokens_app_id_fk` FOREIGN KEY (`app_id`) REFERENCES `bot_tokens`(`app_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `guild_categories` (\n\t`guild_id` text PRIMARY KEY,\n\t`category_id` text,\n\t`audio_category_id` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE IF NOT EXISTS `scheduled_tasks` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT,\n\t`status` text DEFAULT 'planned' NOT NULL,\n\t`schedule_kind` text NOT NULL,\n\t`run_at` datetime,\n\t`cron_expr` text,\n\t`timezone` text,\n\t`next_run_at` datetime NOT NULL,\n\t`running_started_at` datetime,\n\t`last_run_at` datetime,\n\t`last_error` text,\n\t`attempts` integer DEFAULT 0 NOT NULL,\n\t`payload_json` text NOT NULL,\n\t`prompt_preview` text NOT NULL,\n\t`channel_id` text,\n\t`thread_id` text,\n\t`session_id` text,\n\t`project_directory` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_scheduled_tasks_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE ON DELETE SET NULL,\n\tCONSTRAINT `fk_scheduled_tasks_thread_id_thread_sessions_thread_id_fk` FOREIGN KEY (`thread_id`) REFERENCES `thread_sessions`(`thread_id`) ON UPDATE CASCADE ON DELETE SET NULL\n);\n\nCREATE TABLE IF NOT EXISTS `session_sleeps` (\n\t`session_id` text PRIMARY KEY,\n\t`wake_at` datetime NOT NULL,\n\t`reason` text,\n\t`status` text DEFAULT 'planned' NOT NULL,\n\t`delivery_id` text NOT NULL,\n\t`attempts` integer DEFAULT 0 NOT NULL,\n\t`last_attempt_at` datetime,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE IF NOT EXISTS `thread_sessions` (\n\t`thread_id` text PRIMARY KEY,\n\t`session_id` text NOT NULL,\n\t`source` text DEFAULT 'kimaki' NOT NULL,\n\t`last_synced_name` text,\n\t`parent_session_id` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS `scheduled_tasks_status_next_run_at_idx` ON `scheduled_tasks` (`status`,`next_run_at`);\nCREATE INDEX IF NOT EXISTS `scheduled_tasks_channel_id_status_idx` ON `scheduled_tasks` (`channel_id`,`status`);\nCREATE INDEX IF NOT EXISTS `scheduled_tasks_thread_id_status_idx` ON `scheduled_tasks` (`thread_id`,`status`);\nCREATE INDEX IF NOT EXISTS `session_sleeps_status_wake_at_idx` ON `session_sleeps` (`status`,`wake_at`);"
|
|
3
|
+
export const SCHEMA_SQL = "CREATE TABLE IF NOT EXISTS `bot_api_keys` (\n\t`app_id` text PRIMARY KEY,\n\t`gemini_api_key` text,\n\t`openai_api_key` text,\n\t`xai_api_key` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_bot_api_keys_app_id_bot_tokens_app_id_fk` FOREIGN KEY (`app_id`) REFERENCES `bot_tokens`(`app_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `bot_settings` (\n\t`app_id` text PRIMARY KEY,\n\t`voice_call_voice` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_bot_settings_app_id_bot_tokens_app_id_fk` FOREIGN KEY (`app_id`) REFERENCES `bot_tokens`(`app_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `bot_tokens` (\n\t`app_id` text PRIMARY KEY,\n\t`token` text NOT NULL,\n\t`bot_mode` text DEFAULT 'self_hosted' NOT NULL,\n\t`client_id` text,\n\t`client_secret` text,\n\t`proxy_url` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`last_used_at` datetime\n);\n\nCREATE TABLE IF NOT EXISTS `channel_agents` (\n\t`channel_id` text PRIMARY KEY,\n\t`agent_name` text NOT NULL,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_channel_agents_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `channel_coordinator` (\n\t`channel_id` text PRIMARY KEY,\n\t`enabled` integer NOT NULL,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_channel_coordinator_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `channel_directories` (\n\t`channel_id` text PRIMARY KEY,\n\t`directory` text NOT NULL,\n\t`channel_type` text NOT NULL,\n\t`guild_id` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE IF NOT EXISTS `channel_mention_mode` (\n\t`channel_id` text PRIMARY KEY,\n\t`enabled` integer DEFAULT 0 NOT NULL,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_channel_mention_mode_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `channel_models` (\n\t`channel_id` text PRIMARY KEY,\n\t`model_id` text NOT NULL,\n\t`variant` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_channel_models_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `channel_verbosity` (\n\t`channel_id` text PRIMARY KEY,\n\t`verbosity` text DEFAULT 'tools_and_text' NOT NULL,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_channel_verbosity_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `channel_worktrees` (\n\t`channel_id` text PRIMARY KEY,\n\t`enabled` integer DEFAULT 0 NOT NULL,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_channel_worktrees_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `global_models` (\n\t`app_id` text PRIMARY KEY,\n\t`model_id` text NOT NULL,\n\t`variant` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_global_models_app_id_bot_tokens_app_id_fk` FOREIGN KEY (`app_id`) REFERENCES `bot_tokens`(`app_id`) ON UPDATE CASCADE\n);\n\nCREATE TABLE IF NOT EXISTS `guild_categories` (\n\t`guild_id` text PRIMARY KEY,\n\t`category_id` text,\n\t`audio_category_id` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE IF NOT EXISTS `scheduled_tasks` (\n\t`id` integer PRIMARY KEY AUTOINCREMENT,\n\t`status` text DEFAULT 'planned' NOT NULL,\n\t`schedule_kind` text NOT NULL,\n\t`run_at` datetime,\n\t`cron_expr` text,\n\t`timezone` text,\n\t`next_run_at` datetime NOT NULL,\n\t`running_started_at` datetime,\n\t`last_run_at` datetime,\n\t`last_error` text,\n\t`attempts` integer DEFAULT 0 NOT NULL,\n\t`payload_json` text NOT NULL,\n\t`prompt_preview` text NOT NULL,\n\t`channel_id` text,\n\t`thread_id` text,\n\t`session_id` text,\n\t`project_directory` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\tCONSTRAINT `fk_scheduled_tasks_channel_id_channel_directories_channel_id_fk` FOREIGN KEY (`channel_id`) REFERENCES `channel_directories`(`channel_id`) ON UPDATE CASCADE ON DELETE SET NULL,\n\tCONSTRAINT `fk_scheduled_tasks_thread_id_thread_sessions_thread_id_fk` FOREIGN KEY (`thread_id`) REFERENCES `thread_sessions`(`thread_id`) ON UPDATE CASCADE ON DELETE SET NULL\n);\n\nCREATE TABLE IF NOT EXISTS `session_sleeps` (\n\t`session_id` text PRIMARY KEY,\n\t`wake_at` datetime NOT NULL,\n\t`reason` text,\n\t`status` text DEFAULT 'planned' NOT NULL,\n\t`delivery_id` text NOT NULL,\n\t`attempts` integer DEFAULT 0 NOT NULL,\n\t`last_attempt_at` datetime,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE TABLE IF NOT EXISTS `thread_sessions` (\n\t`thread_id` text PRIMARY KEY,\n\t`session_id` text NOT NULL,\n\t`source` text DEFAULT 'kimaki' NOT NULL,\n\t`last_synced_name` text,\n\t`parent_session_id` text,\n\t`created_at` datetime DEFAULT CURRENT_TIMESTAMP,\n\t`updated_at` datetime DEFAULT CURRENT_TIMESTAMP\n);\n\nCREATE INDEX IF NOT EXISTS `scheduled_tasks_status_next_run_at_idx` ON `scheduled_tasks` (`status`,`next_run_at`);\nCREATE INDEX IF NOT EXISTS `scheduled_tasks_channel_id_status_idx` ON `scheduled_tasks` (`channel_id`,`status`);\nCREATE INDEX IF NOT EXISTS `scheduled_tasks_thread_id_status_idx` ON `scheduled_tasks` (`thread_id`,`status`);\nCREATE INDEX IF NOT EXISTS `session_sleeps_status_wake_at_idx` ON `session_sleeps` (`status`,`wake_at`);"
|
package/src/schema.ts
CHANGED
|
@@ -124,6 +124,14 @@ export const channel_mention_mode = s.sqliteTable('channel_mention_mode', {
|
|
|
124
124
|
updated_at: datetime('updated_at').default(orm.sql`CURRENT_TIMESTAMP`).$onUpdate(() => new Date()),
|
|
125
125
|
})
|
|
126
126
|
|
|
127
|
+
// /toggle-coordinator choice of a project channel. Missing: the `--coordinator` default.
|
|
128
|
+
export const channel_coordinator = s.sqliteTable('channel_coordinator', {
|
|
129
|
+
channel_id: s.text('channel_id').primaryKey().notNull().references(() => channel_directories.channel_id, { onUpdate: 'cascade' }),
|
|
130
|
+
enabled: s.integer('enabled', { mode: 'number' }).notNull(),
|
|
131
|
+
created_at: datetime('created_at').default(orm.sql`CURRENT_TIMESTAMP`),
|
|
132
|
+
updated_at: datetime('updated_at').default(orm.sql`CURRENT_TIMESTAMP`).$onUpdate(() => new Date()),
|
|
133
|
+
})
|
|
134
|
+
|
|
127
135
|
export const scheduled_tasks = s.sqliteTable('scheduled_tasks', {
|
|
128
136
|
id: s.integer('id', { mode: 'number' }).primaryKey({ autoIncrement: true }).notNull(),
|
|
129
137
|
status: s.text('status', { enum: ['planned', 'running', 'completed', 'cancelled', 'failed'] }).notNull().default('planned'),
|
|
@@ -176,6 +184,7 @@ export const relations = defineRelations({
|
|
|
176
184
|
channel_worktrees,
|
|
177
185
|
channel_verbosity,
|
|
178
186
|
channel_mention_mode,
|
|
187
|
+
channel_coordinator,
|
|
179
188
|
scheduled_tasks,
|
|
180
189
|
session_sleeps,
|
|
181
190
|
}, (r) => ({
|
|
@@ -203,6 +212,7 @@ export const relations = defineRelations({
|
|
|
203
212
|
channel_worktree: r.one.channel_worktrees({ from: r.channel_directories.channel_id, to: r.channel_worktrees.channel_id }),
|
|
204
213
|
channel_verbosity: r.one.channel_verbosity({ from: r.channel_directories.channel_id, to: r.channel_verbosity.channel_id }),
|
|
205
214
|
channel_mention_mode: r.one.channel_mention_mode({ from: r.channel_directories.channel_id, to: r.channel_mention_mode.channel_id }),
|
|
215
|
+
channel_coordinator: r.one.channel_coordinator({ from: r.channel_directories.channel_id, to: r.channel_coordinator.channel_id }),
|
|
206
216
|
scheduled_tasks: r.many.scheduled_tasks(),
|
|
207
217
|
},
|
|
208
218
|
channel_models: {
|
|
@@ -220,6 +230,9 @@ export const relations = defineRelations({
|
|
|
220
230
|
channel_mention_mode: {
|
|
221
231
|
channel: r.one.channel_directories({ from: r.channel_mention_mode.channel_id, to: r.channel_directories.channel_id }),
|
|
222
232
|
},
|
|
233
|
+
channel_coordinator: {
|
|
234
|
+
channel: r.one.channel_directories({ from: r.channel_coordinator.channel_id, to: r.channel_directories.channel_id }),
|
|
235
|
+
},
|
|
223
236
|
scheduled_tasks: {
|
|
224
237
|
channel: r.one.channel_directories({ from: r.scheduled_tasks.channel_id, to: r.channel_directories.channel_id }),
|
|
225
238
|
thread: r.one.thread_sessions({ from: r.scheduled_tasks.thread_id, to: r.thread_sessions.thread_id }),
|