kimaki 1.4.0 → 1.6.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 +10 -1
- package/dist/bot.js +14 -6
- package/dist/bot.js.map +1 -1
- package/dist/btw.e2e.test.js +3 -3
- package/dist/builtin-skills.d.ts +1 -1
- package/dist/builtin-skills.js +1 -1
- package/dist/builtin-skills.js.map +1 -1
- package/dist/builtin-skills.test.js +3 -4
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/catalog-refresh.e2e.test.js +2 -2
- package/dist/cli/bot.js +25 -14
- 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 +4 -5
- 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.js +135 -0
- package/dist/coordinator-agents.e2e.test.js.map +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.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 +75 -0
- 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 +1 -1
- package/dist/event-loop.js +73 -43
- 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 +68 -13
- 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 +68 -16
- 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 +4 -2
- package/dist/main.js +13 -3
- package/dist/main.js.map +1 -1
- package/dist/markdown/markdown.test.js +4 -4
- package/dist/markdown/render-markdown.js +6 -5
- 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 +76 -17
- package/dist/plugin/index.js.map +1 -1
- 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/prompt.d.ts +21 -8
- 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 +1 -1
- 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 +146 -0
- 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 +19 -7
- package/dist/session-events.js +166 -22
- package/dist/session-events.js.map +1 -1
- package/dist/session-ops.e2e.test.js +88 -11
- package/dist/session-ops.e2e.test.js.map +1 -1
- package/dist/sessions.d.ts +41 -18
- package/dist/sessions.js +174 -63
- package/dist/sessions.js.map +1 -1
- package/dist/shell.e2e.test.js +4 -4
- package/dist/slash-commands.js +12 -6
- 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 +1 -0
- 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 +3 -1
- package/dist/system-prompt.js +138 -64
- package/dist/system-prompt.js.map +1 -1
- 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 -1
- package/dist/test/harness.js +51 -10
- package/dist/test/harness.js.map +1 -1
- package/dist/test/replay.js +11 -7
- package/dist/test/replay.js.map +1 -1
- package/dist/thread-reducer.d.ts +29 -3
- package/dist/thread-reducer.fixtures.test.js +58 -51
- package/dist/thread-reducer.fixtures.test.js.map +1 -1
- package/dist/thread-reducer.js +112 -25
- package/dist/thread-reducer.js.map +1 -1
- package/dist/thread-reducer.test.js +197 -24
- package/dist/thread-reducer.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.js +80 -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 +12 -6
- 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 +29 -5
- package/src/btw.e2e.test.ts +3 -3
- package/src/builtin-skills.test.ts +3 -4
- package/src/builtin-skills.ts +1 -1
- package/src/catalog-refresh.e2e.test.ts +2 -2
- package/src/cli/bot.ts +23 -14
- 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 +168 -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 -42
- 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 +62 -12
- 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 +22 -7
- package/src/markdown/markdown.test.ts +4 -4
- package/src/markdown/render-markdown.ts +6 -5
- 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 +68 -15
- 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 +160 -21
- package/src/session-ops.e2e.test.ts +90 -10
- package/src/sessions.ts +214 -64
- package/src/shell.e2e.test.ts +4 -4
- package/src/slash-commands.test.ts +2 -0
- package/src/slash-commands.ts +12 -6
- 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 +143 -63
- package/src/test/generate-log-plugin/index.ts +16 -0
- package/src/test/harness.ts +77 -14
- package/src/test/replay.ts +12 -6
- package/src/thread-reducer.fixtures.test.ts +58 -51
- package/src/thread-reducer.test.ts +207 -26
- package/src/thread-reducer.ts +138 -37
- 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 +101 -26
- package/src/working-directory.e2e.test.ts +11 -11
- package/dist/message-tool.e2e.test.js +0 -39
- package/dist/message-tool.e2e.test.js.map +0 -1
- package/dist/plugin/read-media.d.ts +0 -23
- package/dist/plugin/read-media.js +0 -72
- package/dist/plugin/read-media.js.map +0 -1
- package/dist/plugin/session-id/index.js +0 -18
- package/dist/plugin/session-id/index.js.map +0 -1
- package/dist/probe-live.tmp.js +0 -82
- package/dist/probe-live.tmp.js.map +0 -1
- package/dist/tasks.e2e.test.js +0 -173
- package/dist/tasks.e2e.test.js.map +0 -1
- /package/dist/{message-tool.e2e.test.d.ts → coordinator-agents.e2e.test.d.ts} +0 -0
- /package/dist/{probe-live.tmp.d.ts → coordinator-idle.e2e.test.d.ts} +0 -0
- /package/dist/{tasks.e2e.test.d.ts → coordinator-queue.e2e.test.d.ts} +0 -0
- /package/dist/{plugin/session-id → test/generate-log-plugin}/index.d.ts +0 -0
package/src/sessions.ts
CHANGED
|
@@ -6,7 +6,7 @@ import path from 'node:path'
|
|
|
6
6
|
import crypto from 'node:crypto'
|
|
7
7
|
import { setTimeout as sleep } from 'node:timers/promises'
|
|
8
8
|
import type { SessionMetadata } from '@opencode/client'
|
|
9
|
-
import type { TextChannel
|
|
9
|
+
import type { TextChannel } from 'discord.js'
|
|
10
10
|
import * as orm from 'drizzle-orm'
|
|
11
11
|
|
|
12
12
|
import {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
globalModel,
|
|
15
15
|
oc,
|
|
16
16
|
parseModel,
|
|
17
|
+
projectChannelOf,
|
|
17
18
|
projectOf,
|
|
18
19
|
readMarker,
|
|
19
20
|
rootSession,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
textChannel,
|
|
23
24
|
type Author,
|
|
24
25
|
type Bot,
|
|
26
|
+
type Conversation,
|
|
25
27
|
type ModelChoice,
|
|
26
28
|
type PromptFile,
|
|
27
29
|
} from './bot.ts'
|
|
@@ -29,7 +31,7 @@ import { ConfigError, DbError, DiscordError, OpenCodeError, OpenCodeUnavailableE
|
|
|
29
31
|
import { SILENT_MESSAGE_FLAGS } from './format-parts.ts'
|
|
30
32
|
import { createLogger } from './logger.ts'
|
|
31
33
|
import type { OpenCodeClient, OpencodeEndpoint } from './opencode-server.ts'
|
|
32
|
-
import { prompt, runInSession } from './prompt.ts'
|
|
34
|
+
import { prompt, runInSession, type Sender } from './prompt.ts'
|
|
33
35
|
import { formatEcho } from './queue.ts'
|
|
34
36
|
import type { Route } from './routes.ts'
|
|
35
37
|
import * as schema from './schema.ts'
|
|
@@ -73,14 +75,14 @@ export function btwPrompt({
|
|
|
73
75
|
].join('\n')
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
// Project channel, project directory and current cwd of a session
|
|
77
|
-
export async function
|
|
78
|
-
const channelId =
|
|
79
|
-
if (!channelId) return new DiscordError({ operation: `find the channel of
|
|
78
|
+
// Project channel, project directory and current cwd of a conversation's root session.
|
|
79
|
+
export async function conversationProject(bot: Bot, conversation: Conversation) {
|
|
80
|
+
const channelId = projectChannelOf(conversation)
|
|
81
|
+
if (!channelId) return new DiscordError({ operation: `find the channel of conversation ${conversation.id}` })
|
|
80
82
|
const row = await projectOf(bot, channelId)
|
|
81
83
|
if (row instanceof Error) return row
|
|
82
84
|
if (!row) return new DiscordError({ operation: `find the project of channel ${channelId}` })
|
|
83
|
-
const sessionId = rootSession(bot,
|
|
85
|
+
const sessionId = rootSession(bot, conversation.id)
|
|
84
86
|
if (sessionId instanceof Error) return sessionId
|
|
85
87
|
const directory = await sessionDirectory(bot, sessionId)
|
|
86
88
|
if (directory instanceof Error) return directory
|
|
@@ -89,8 +91,8 @@ export async function threadProject(bot: Bot, thread: ThreadChannel) {
|
|
|
89
91
|
|
|
90
92
|
// /cwd and `kimaki session cwd`: show the cwd, or move the session to a
|
|
91
93
|
// project subfolder or linked worktree.
|
|
92
|
-
export async function sessionCwd(bot: Bot, {
|
|
93
|
-
const project = await
|
|
94
|
+
export async function sessionCwd(bot: Bot, { conversation, directory }: { conversation: Conversation; directory?: string }) {
|
|
95
|
+
const project = await conversationProject(bot, conversation)
|
|
94
96
|
if (project instanceof Error) return project
|
|
95
97
|
if (!directory) return { directory: project.directory }
|
|
96
98
|
const destination = await resolveWorkingDirectory({
|
|
@@ -100,7 +102,7 @@ export async function sessionCwd(bot: Bot, { thread, directory }: { thread: Thre
|
|
|
100
102
|
if (destination instanceof Error) return destination
|
|
101
103
|
const plugin = await bot.features.waitForPlugin(destination)
|
|
102
104
|
if (plugin instanceof Error) return plugin
|
|
103
|
-
const sessionId = rootSession(bot,
|
|
105
|
+
const sessionId = rootSession(bot, conversation.id)
|
|
104
106
|
if (sessionId instanceof Error) return sessionId
|
|
105
107
|
const moved = await oc(bot, 'session.move', (client) => client.session.move({ sessionID: sessionId, directory: destination, delivery: 'steer' }))
|
|
106
108
|
if (moved instanceof Error) return moved
|
|
@@ -128,8 +130,9 @@ export async function catalogReady(bot: Pick<Bot, 'opencode'>, directory: string
|
|
|
128
130
|
if (ready instanceof Error) return ready
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
// Agents a session may switch to (not subagents, not hidden ones).
|
|
132
|
-
|
|
133
|
+
// Agents a session may switch to (not subagents, not hidden ones). OpenCode lists
|
|
134
|
+
// its default agent (`default_agent`, else build) first: core/src/agent.ts `list`.
|
|
135
|
+
export async function primaryAgents(bot: Pick<Bot, 'opencode'>, directory: string) {
|
|
133
136
|
const ready = await catalogReady(bot, directory)
|
|
134
137
|
if (ready instanceof Error) return ready
|
|
135
138
|
const agents = await oc(bot, 'agent.list', (client) => client.agent.list({ location: { directory } }))
|
|
@@ -137,6 +140,19 @@ export async function primaryAgents(bot: Bot, directory: string) {
|
|
|
137
140
|
return agents.data.filter((agent) => agent.mode !== 'subagent' && !agent.hidden)
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
// What a new session of the channel starts with: its project folder, and the channel agent or OpenCode's default agent.
|
|
144
|
+
export async function channelDefaults(bot: Pick<Bot, 'db' | 'opencode'>, channelId: string) {
|
|
145
|
+
const row = await bot.db.query.channel_directories
|
|
146
|
+
.findFirst({ where: { channel_id: channelId }, with: { channel_agent: true } })
|
|
147
|
+
.catch((cause) => new DbError({ operation: 'read channel defaults', cause }))
|
|
148
|
+
if (row instanceof Error) return row
|
|
149
|
+
if (!row) return null
|
|
150
|
+
if (row.channel_agent) return { projectDirectory: row.directory, defaultAgent: row.channel_agent.agent_name }
|
|
151
|
+
const agents = await primaryAgents(bot, row.directory)
|
|
152
|
+
if (agents instanceof Error) return agents
|
|
153
|
+
return { projectDirectory: row.directory, defaultAgent: agents[0]?.id ?? null }
|
|
154
|
+
}
|
|
155
|
+
|
|
140
156
|
export async function agentSelection(bot: Bot, { directory, agent }: { directory: string; agent: string }) {
|
|
141
157
|
const agents = await primaryAgents(bot, directory)
|
|
142
158
|
if (agents instanceof Error) return agents
|
|
@@ -149,10 +165,29 @@ export async function agentSelection(bot: Bot, { directory, agent }: { directory
|
|
|
149
165
|
// OpenCode switches the agent and model separately, including the configured variant.
|
|
150
166
|
export async function switchAgent(
|
|
151
167
|
bot: Bot,
|
|
152
|
-
{
|
|
168
|
+
{
|
|
169
|
+
sessionId,
|
|
170
|
+
directory,
|
|
171
|
+
agent,
|
|
172
|
+
model,
|
|
173
|
+
reselect = false,
|
|
174
|
+
}: {
|
|
175
|
+
sessionId: string
|
|
176
|
+
directory: string
|
|
177
|
+
agent: string
|
|
178
|
+
model?: ModelChoice | null
|
|
179
|
+
// An explicit agent pick by the user (/agent, /<agent>-agent): apply the agent's model even when unchanged.
|
|
180
|
+
reselect?: boolean
|
|
181
|
+
},
|
|
153
182
|
) {
|
|
154
183
|
const selected = await agentSelection(bot, { directory, agent })
|
|
155
184
|
if (selected instanceof Error) return selected
|
|
185
|
+
// naming the session's current agent again (agents pass --agent <current_agent>
|
|
186
|
+
// on every send) keeps the model the session runs on, e.g. one set with /model.
|
|
187
|
+
if (!model && !reselect) {
|
|
188
|
+
const info = await oc(bot, 'session.get', (client) => client.session.get({ sessionID: sessionId }))
|
|
189
|
+
if (!(info instanceof Error) && info.agent === selected.agent) return { ...selected, model: null }
|
|
190
|
+
}
|
|
156
191
|
const choice = model ?? selected.model
|
|
157
192
|
if (choice) {
|
|
158
193
|
const switched = await oc(bot, 'session.switchModel', (client) => client.session.switchModel({
|
|
@@ -171,7 +206,7 @@ async function putInstructions(
|
|
|
171
206
|
bot: Bot,
|
|
172
207
|
{
|
|
173
208
|
sessionId,
|
|
174
|
-
|
|
209
|
+
conversation,
|
|
175
210
|
channel,
|
|
176
211
|
directory,
|
|
177
212
|
userId,
|
|
@@ -179,7 +214,7 @@ async function putInstructions(
|
|
|
179
214
|
scheduledTask = null,
|
|
180
215
|
}: {
|
|
181
216
|
sessionId: string
|
|
182
|
-
|
|
217
|
+
conversation: Conversation
|
|
183
218
|
channel: TextChannel
|
|
184
219
|
directory: string
|
|
185
220
|
userId: string
|
|
@@ -187,14 +222,14 @@ async function putInstructions(
|
|
|
187
222
|
scheduledTask?: ScheduledRun | null
|
|
188
223
|
},
|
|
189
224
|
): Promise<OpenCodeUnavailableError | OpenCodeError | void> {
|
|
190
|
-
// The agent list is optional prompt context: a failure must not strand the
|
|
225
|
+
// The agent list is optional prompt context: a failure must not strand the conversation.
|
|
191
226
|
const found = await primaryAgents(bot, directory)
|
|
192
227
|
if (found instanceof Error) logger.warn(`agent list for instructions failed`, found)
|
|
193
228
|
// The ID is what switchAgent and session.create take.
|
|
194
229
|
const agents = found instanceof Error ? [] : found.map((agent) => ({ name: agent.id, description: agent.description ?? '' }))
|
|
195
230
|
const value = baseInstructions({
|
|
196
231
|
sessionId,
|
|
197
|
-
threadId:
|
|
232
|
+
threadId: conversation.id,
|
|
198
233
|
channelId: channel.id,
|
|
199
234
|
guildId: channel.guildId,
|
|
200
235
|
userId,
|
|
@@ -203,6 +238,8 @@ async function putInstructions(
|
|
|
203
238
|
agents,
|
|
204
239
|
parentSessionId,
|
|
205
240
|
scheduledTask,
|
|
241
|
+
// Derived from where the session lives, never from bindings that can be stale.
|
|
242
|
+
coordinator: conversation.id === channel.id,
|
|
206
243
|
disabled: bot.disabledPrompt,
|
|
207
244
|
})
|
|
208
245
|
const put = await oc(bot, 'instructions.entry.put', (client) =>
|
|
@@ -225,6 +262,7 @@ export function createPluginWait(bot: Pick<Bot, 'opencode'>): PluginWait {
|
|
|
225
262
|
if (endpoint && active.get(endpoint)?.has(directory)) return
|
|
226
263
|
const location = { directory }
|
|
227
264
|
const deadline = Date.now() + 10_000
|
|
265
|
+
let loadError: string | undefined
|
|
228
266
|
while (Date.now() < deadline) {
|
|
229
267
|
const endpoint = bot.opencode.endpoint
|
|
230
268
|
if (!endpoint) return new OpenCodeUnavailableError({ reason: 'not connected' })
|
|
@@ -240,8 +278,18 @@ export function createPluginWait(bot: Pick<Bot, 'opencode'>): PluginWait {
|
|
|
240
278
|
active.set(endpoint, (active.get(endpoint) ?? new Set()).add(directory))
|
|
241
279
|
return
|
|
242
280
|
}
|
|
281
|
+
// A plugin that fails to load has no ID: find the shim directory by its path.
|
|
282
|
+
const failed = plugins.data.find((plugin) => plugin.source.type === 'local' && path.basename(plugin.source.path) === 'kimaki')
|
|
283
|
+
loadError = failed?.state.status === 'failed' ? (failed.state.ref ?? failed.state.error) : undefined
|
|
243
284
|
await sleep(200)
|
|
244
285
|
}
|
|
286
|
+
if (loadError) {
|
|
287
|
+
// TODO: remove the restart hint once OpenCode's Bun evicts failed ESM loads; Bun 1.4.4 keeps
|
|
288
|
+
// them forever, so a reload during `pnpm build` stays broken after the build ends.
|
|
289
|
+
return new ConfigError({
|
|
290
|
+
reason: `Kimaki plugin failed to load in OpenCode (${loadError}); the OpenCode log has the cause. If it names code that no longer exists, OpenCode kept a stale failed load: run \`opencode service restart\``,
|
|
291
|
+
})
|
|
292
|
+
}
|
|
245
293
|
return new ConfigError({
|
|
246
294
|
reason: `Kimaki plugin is not active in OpenCode for ${directory}. Kimaki writes it to <OpenCode config dir>/plugins/kimaki/ on start; check \`opencode plugin list\` and the OpenCode logs`,
|
|
247
295
|
})
|
|
@@ -257,13 +305,13 @@ export async function ensureSessionMarker(
|
|
|
257
305
|
bot: Bot,
|
|
258
306
|
{
|
|
259
307
|
sessionId,
|
|
260
|
-
|
|
308
|
+
conversation,
|
|
261
309
|
channelId,
|
|
262
310
|
directory,
|
|
263
311
|
userId,
|
|
264
312
|
}: {
|
|
265
313
|
sessionId: string
|
|
266
|
-
|
|
314
|
+
conversation: Conversation
|
|
267
315
|
channelId: string
|
|
268
316
|
directory: string
|
|
269
317
|
userId: string
|
|
@@ -277,13 +325,13 @@ export async function ensureSessionMarker(
|
|
|
277
325
|
if (marker && marker.dataDir === bot.dataDir && marker.lockPort === bot.lockPort) return
|
|
278
326
|
// Instructions first: the marker means "set up", so a failure here is retried on the next input.
|
|
279
327
|
if (!marker) {
|
|
280
|
-
logger.log(`adopting legacy session ${sessionId} of
|
|
328
|
+
logger.log(`adopting legacy session ${sessionId} of conversation ${conversation.id}`)
|
|
281
329
|
const channel = await textChannel(bot, channelId)
|
|
282
330
|
if (channel instanceof Error) return channel
|
|
283
|
-
const instructions = await putInstructions(bot, { sessionId,
|
|
331
|
+
const instructions = await putInstructions(bot, { sessionId, conversation, channel, directory, userId })
|
|
284
332
|
if (instructions instanceof Error) return instructions
|
|
285
333
|
}
|
|
286
|
-
const kimaki = { ...marker?.fields, source: 'discord', ...cliContext(bot), threadId:
|
|
334
|
+
const kimaki = { ...marker?.fields, source: 'discord', ...cliContext(bot), threadId: conversation.id, channelId }
|
|
287
335
|
const updated = await oc(bot, 'session.update', (client) =>
|
|
288
336
|
client.session.update({ sessionID: sessionId, metadata: { ...info.metadata, kimaki } }),
|
|
289
337
|
)
|
|
@@ -311,6 +359,30 @@ export async function refreshCliContext(bot: Bot) {
|
|
|
311
359
|
}
|
|
312
360
|
}
|
|
313
361
|
|
|
362
|
+
type ChannelDefaults = {
|
|
363
|
+
channel_model?: { model_id: string; variant: string | null } | null
|
|
364
|
+
channel_agent?: { agent_name: string } | null
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Agent and model of a new session: an agent the input asked for, else the
|
|
368
|
+
// channel agent; the model from the input, that agent, the channel, the global default.
|
|
369
|
+
async function agentAndModel(
|
|
370
|
+
bot: Bot,
|
|
371
|
+
{ defaults, route, directory, explicitModel }: { defaults: ChannelDefaults | undefined; route: Route; directory: string; explicitModel?: ModelChoice },
|
|
372
|
+
) {
|
|
373
|
+
const requestedAgent = (route.kind === 'steer' && route.agent) || defaults?.channel_agent?.agent_name
|
|
374
|
+
const selected = requestedAgent ? await agentSelection(bot, { directory, agent: requestedAgent }) : null
|
|
375
|
+
if (selected instanceof Error) return selected
|
|
376
|
+
const agentModel = route.kind === 'steer' && route.agent ? selected?.model : null
|
|
377
|
+
const global = await globalModel(bot)
|
|
378
|
+
if (global instanceof Error) return global
|
|
379
|
+
const choice = explicitModel ?? agentModel ?? parseModel(defaults?.channel_model?.model_id, defaults?.channel_model?.variant) ?? selected?.model ?? global
|
|
380
|
+
const model = choice
|
|
381
|
+
? { providerID: choice.providerID, id: choice.id, ...(choice.variant && { variant: choice.variant }) }
|
|
382
|
+
: undefined
|
|
383
|
+
return { model, agent: selected?.agent }
|
|
384
|
+
}
|
|
385
|
+
|
|
314
386
|
export async function startSession(
|
|
315
387
|
bot: Bot,
|
|
316
388
|
{
|
|
@@ -318,6 +390,7 @@ export async function startSession(
|
|
|
318
390
|
directory: requestedDirectory,
|
|
319
391
|
route,
|
|
320
392
|
author,
|
|
393
|
+
members = [],
|
|
321
394
|
messageId,
|
|
322
395
|
startMessageId = messageId,
|
|
323
396
|
showInput = startMessageId === null,
|
|
@@ -326,6 +399,7 @@ export async function startSession(
|
|
|
326
399
|
model: explicitModel,
|
|
327
400
|
permissions,
|
|
328
401
|
parentSessionId,
|
|
402
|
+
sender,
|
|
329
403
|
task,
|
|
330
404
|
worktree,
|
|
331
405
|
baseBranch,
|
|
@@ -336,6 +410,8 @@ export async function startSession(
|
|
|
336
410
|
// The first input: a prompt, a shell command or an OpenCode command.
|
|
337
411
|
route: Exclude<Route, { kind: 'btw' | 'new-session' | 'queue' }>
|
|
338
412
|
author: Author
|
|
413
|
+
// More Discord users to add to the thread, next to the author (`kimaki send --user`).
|
|
414
|
+
members?: readonly string[]
|
|
339
415
|
// The Discord message this input came from; it keys the prompt id.
|
|
340
416
|
messageId: string
|
|
341
417
|
// The thread starts from this message; null for a thread without one.
|
|
@@ -350,12 +426,13 @@ export async function startSession(
|
|
|
350
426
|
model?: ModelChoice
|
|
351
427
|
permissions?: PermissionRules
|
|
352
428
|
parentSessionId?: string
|
|
429
|
+
sender?: Sender
|
|
353
430
|
// A scheduled run (scheduler.ts): marks the session as started by this task.
|
|
354
431
|
task?: ScheduledRun
|
|
355
432
|
// undefined uses the channel default; false explicitly reuses an existing cwd.
|
|
356
433
|
worktree?: string | false
|
|
357
434
|
baseBranch?: string
|
|
358
|
-
// false: only set up the thread (/new-worktree), no first input.
|
|
435
|
+
// false: only set up the thread (/new-worktree, /new-session without a prompt), no first input.
|
|
359
436
|
run?: boolean
|
|
360
437
|
},
|
|
361
438
|
): Promise<Error | { threadId: string; sessionId: string }> {
|
|
@@ -368,7 +445,7 @@ export async function startSession(
|
|
|
368
445
|
const useWorktree = typeof worktree === 'string' || (worktree !== false && channelWorktrees)
|
|
369
446
|
const textSlug = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40).replace(/^-|-$/g, '') || 'session'
|
|
370
447
|
const slug = typeof worktree === 'string' && worktree ? worktree : `${textSlug}-${crypto.randomBytes(4).toString('hex')}`
|
|
371
|
-
const threadName = explicitName ?? (useWorktree ?
|
|
448
|
+
const threadName = explicitName ?? (useWorktree ? slug : text.replace(/\s+/g, ' ').slice(0, 80) || 'Kimaki session')
|
|
372
449
|
|
|
373
450
|
// The thread shows in Discord while the checkout, plugin, agent and model are resolved.
|
|
374
451
|
const creating = (async () => {
|
|
@@ -378,8 +455,11 @@ export async function startSession(
|
|
|
378
455
|
.create({ name: threadName, autoArchiveDuration: 1440, ...(startMessageId && { startMessage: startMessageId }) })
|
|
379
456
|
.catch((cause) => new DiscordError({ operation: 'create thread', cause }))
|
|
380
457
|
if (thread instanceof Error) return thread
|
|
381
|
-
// Discord only adds the thread creator (the bot);
|
|
382
|
-
|
|
458
|
+
// Discord only adds the thread creator (the bot); users must be added to see it in the sidebar.
|
|
459
|
+
// Not awaited: the session does not need them, and each add is a Discord round trip.
|
|
460
|
+
for (const id of new Set([author.id, ...members])) {
|
|
461
|
+
if (id !== bot.discord.user?.id) void thread.members.add(id).catch((e: Error) => logger.warn(`add member ${id}`, e))
|
|
462
|
+
}
|
|
383
463
|
return { channel, thread }
|
|
384
464
|
})()
|
|
385
465
|
const setup = await (async () => {
|
|
@@ -390,17 +470,9 @@ export async function startSession(
|
|
|
390
470
|
const directory = checkout?.directory ?? requestedDirectory
|
|
391
471
|
const plugin = await bot.features.waitForPlugin(directory)
|
|
392
472
|
if (plugin instanceof Error) return plugin
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const agentModel = route.kind === 'steer' && route.agent ? selected?.model : null
|
|
397
|
-
const global = await globalModel(bot)
|
|
398
|
-
if (global instanceof Error) return global
|
|
399
|
-
const choice = explicitModel ?? agentModel ?? parseModel(defaults?.channel_model?.model_id, defaults?.channel_model?.variant) ?? selected?.model ?? global
|
|
400
|
-
const model = choice
|
|
401
|
-
? { providerID: choice.providerID, id: choice.id, ...(choice.variant && { variant: choice.variant }) }
|
|
402
|
-
: undefined
|
|
403
|
-
return { checkout, directory, model, agent: selected?.agent }
|
|
473
|
+
const chosen = await agentAndModel(bot, { defaults, route, directory, explicitModel })
|
|
474
|
+
if (chosen instanceof Error) return chosen
|
|
475
|
+
return { checkout, directory, ...chosen }
|
|
404
476
|
})()
|
|
405
477
|
const created = await creating
|
|
406
478
|
if (created instanceof Error) return created
|
|
@@ -416,6 +488,7 @@ export async function startSession(
|
|
|
416
488
|
channelId,
|
|
417
489
|
source: task ? 'task' : 'discord',
|
|
418
490
|
...(task && { taskId: task.id, task }),
|
|
491
|
+
...(bot.warmupTimers && { warmup: bot.warmupTimers }),
|
|
419
492
|
...cliContext(bot),
|
|
420
493
|
...(parentSessionId && { parentSessionId }),
|
|
421
494
|
}
|
|
@@ -434,7 +507,7 @@ export async function startSession(
|
|
|
434
507
|
|
|
435
508
|
const instructions = await putInstructions(bot, {
|
|
436
509
|
sessionId: session.id,
|
|
437
|
-
thread,
|
|
510
|
+
conversation: thread,
|
|
438
511
|
channel,
|
|
439
512
|
directory,
|
|
440
513
|
userId: author.id,
|
|
@@ -446,9 +519,10 @@ export async function startSession(
|
|
|
446
519
|
const bound = await bindThread(bot, { threadId: thread.id, sessionId: session.id, channelId, directory, isNew: true })
|
|
447
520
|
if (bound instanceof Error) return bound
|
|
448
521
|
if (!run) {
|
|
522
|
+
const ready = checkout ? `Worktree ready: \`${directory}\`\nBranch: \`${checkout.branch ?? 'detached'}\`\n` : ''
|
|
449
523
|
const posted = await thread
|
|
450
524
|
.send({
|
|
451
|
-
content:
|
|
525
|
+
content: `${ready}Send a message to start working.`,
|
|
452
526
|
allowedMentions: { parse: [] },
|
|
453
527
|
flags: SILENT_MESSAGE_FLAGS,
|
|
454
528
|
})
|
|
@@ -459,25 +533,88 @@ export async function startSession(
|
|
|
459
533
|
if (showInput) {
|
|
460
534
|
const names = files.map((file) => file.name).join(', ')
|
|
461
535
|
const shown = names ? `${text}\nFiles: ${names}` : text
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (echo instanceof Error) return echo
|
|
536
|
+
// Through the thread's effect queue, so the prompt does not wait for Discord,
|
|
537
|
+
// the echo comes before the session output and a long prompt is split.
|
|
538
|
+
bot.effects.run(thread.id, [{ type: 'send', text: formatEcho({ username: author.username, text: shown }), echo: true }])
|
|
466
539
|
}
|
|
467
540
|
|
|
468
|
-
const
|
|
541
|
+
const base = { sessionId: session.id, threadId: thread.id, threadName, author, messageId, files, sender }
|
|
542
|
+
// A new session has no run, question or permission to cancel: prompt directly instead of steer().
|
|
543
|
+
const sent = route.kind === 'steer'
|
|
544
|
+
? await prompt(bot, { ...base, text: route.text, delivery: 'steer' })
|
|
545
|
+
: await runInSession(bot, { ...base, directory, route })
|
|
546
|
+
if (sent instanceof Error) return sent
|
|
547
|
+
return { threadId: thread.id, sessionId: session.id }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// The first message of a coordinator channel: one long session that lives in the
|
|
551
|
+
// channel itself, bound like a thread with the channel ID as its conversation.
|
|
552
|
+
export async function startCoordinator(
|
|
553
|
+
bot: Bot,
|
|
554
|
+
{
|
|
555
|
+
channel,
|
|
556
|
+
route,
|
|
557
|
+
author,
|
|
558
|
+
messageId,
|
|
559
|
+
files = [],
|
|
560
|
+
}: {
|
|
561
|
+
channel: TextChannel
|
|
562
|
+
route: Exclude<Route, { kind: 'btw' | 'new-session' | 'queue' }>
|
|
563
|
+
author: Author
|
|
564
|
+
messageId: string
|
|
565
|
+
files?: readonly PromptFile[]
|
|
566
|
+
},
|
|
567
|
+
): Promise<Error | { threadId: string; sessionId: string }> {
|
|
568
|
+
const defaults = await bot.db.query.channel_directories
|
|
569
|
+
.findFirst({ where: { channel_id: channel.id }, with: { channel_model: true, channel_agent: true } })
|
|
570
|
+
.catch((cause) => new DbError({ operation: 'read channel defaults', cause }))
|
|
571
|
+
if (defaults instanceof Error) return defaults
|
|
572
|
+
if (!defaults) return new ConfigError({ reason: 'This channel is not configured with a project directory' })
|
|
573
|
+
const directory = defaults.directory
|
|
574
|
+
const plugin = await bot.features.waitForPlugin(directory)
|
|
575
|
+
if (plugin instanceof Error) return plugin
|
|
576
|
+
const chosen = await agentAndModel(bot, { defaults, route, directory })
|
|
577
|
+
if (chosen instanceof Error) return chosen
|
|
578
|
+
const title = `coordinator: ${channel.name}`
|
|
579
|
+
const session = await oc(bot, 'session.create', (client) =>
|
|
580
|
+
client.session.create({
|
|
581
|
+
title,
|
|
582
|
+
location: { directory },
|
|
583
|
+
...(chosen.model && { model: chosen.model }),
|
|
584
|
+
...(chosen.agent && { agent: chosen.agent }),
|
|
585
|
+
permissions: [NO_QUESTION_TOOL, ...disabledSkillRules(bot.disabledSkills)],
|
|
586
|
+
metadata: { kimaki: { threadId: channel.id, channelId: channel.id, source: 'discord', ...(bot.warmupTimers && { warmup: bot.warmupTimers }), ...cliContext(bot) } },
|
|
587
|
+
}),
|
|
588
|
+
)
|
|
589
|
+
if (session instanceof Error) return session
|
|
590
|
+
bot.analytics.track('session_created', { has_worktree: false, source: 'discord' })
|
|
591
|
+
const instructions = await putInstructions(bot, { sessionId: session.id, conversation: channel, channel, directory, userId: author.id })
|
|
592
|
+
if (instructions instanceof Error) return instructions
|
|
593
|
+
const bound = await bindThread(bot, { threadId: channel.id, sessionId: session.id, channelId: channel.id, directory, isNew: true })
|
|
594
|
+
if (bound instanceof Error) return bound
|
|
469
595
|
const sent = await runInSession(bot, {
|
|
470
596
|
sessionId: session.id,
|
|
471
|
-
threadId:
|
|
472
|
-
threadName,
|
|
597
|
+
threadId: channel.id,
|
|
598
|
+
threadName: channel.name,
|
|
473
599
|
directory,
|
|
474
|
-
route
|
|
600
|
+
route,
|
|
475
601
|
author,
|
|
476
602
|
messageId,
|
|
477
603
|
files,
|
|
478
604
|
})
|
|
479
605
|
if (sent instanceof Error) return sent
|
|
480
|
-
return { threadId:
|
|
606
|
+
return { threadId: channel.id, sessionId: session.id }
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// /reset-coordinator: the channel forgets its coordinator. The next message starts a fresh
|
|
610
|
+
// one; the old session stays in OpenCode for /resume.
|
|
611
|
+
export async function endCoordinator(bot: Bot, channelId: string): Promise<DbError | void> {
|
|
612
|
+
const deleted = await bot.db
|
|
613
|
+
.delete(schema.thread_sessions)
|
|
614
|
+
.where(orm.eq(schema.thread_sessions.thread_id, channelId))
|
|
615
|
+
.catch((cause) => new DbError({ operation: 'delete coordinator binding', cause }))
|
|
616
|
+
if (deleted instanceof Error) return deleted
|
|
617
|
+
bot.eventLoop.unbind(channelId)
|
|
481
618
|
}
|
|
482
619
|
|
|
483
620
|
// --- Sessions that get a new thread: /resume, /fork, /fork-subagent, btw.
|
|
@@ -542,7 +679,7 @@ async function adoptSession(
|
|
|
542
679
|
if (marked instanceof Error) return marked
|
|
543
680
|
const instructions = await putInstructions(bot, {
|
|
544
681
|
sessionId,
|
|
545
|
-
thread,
|
|
682
|
+
conversation: thread,
|
|
546
683
|
channel,
|
|
547
684
|
directory: session.location.directory,
|
|
548
685
|
userId: author.id,
|
|
@@ -643,7 +780,7 @@ export async function resume(bot: Bot, { channelId, sessionId, author }: { chann
|
|
|
643
780
|
export async function fork(
|
|
644
781
|
bot: Bot,
|
|
645
782
|
{
|
|
646
|
-
|
|
783
|
+
source,
|
|
647
784
|
sessionId,
|
|
648
785
|
before,
|
|
649
786
|
subagent,
|
|
@@ -651,7 +788,7 @@ export async function fork(
|
|
|
651
788
|
author,
|
|
652
789
|
worktree,
|
|
653
790
|
}: {
|
|
654
|
-
|
|
791
|
+
source: Conversation
|
|
655
792
|
sessionId: string
|
|
656
793
|
before?: string
|
|
657
794
|
// Thread name; default is OpenCode's fork title.
|
|
@@ -663,7 +800,7 @@ export async function fork(
|
|
|
663
800
|
worktree?: { name: string; baseBranch?: string }
|
|
664
801
|
},
|
|
665
802
|
) {
|
|
666
|
-
const root = rootSession(bot,
|
|
803
|
+
const root = rootSession(bot, source.id)
|
|
667
804
|
if (root instanceof Error) return root
|
|
668
805
|
if (sessionId !== root) {
|
|
669
806
|
const info = await oc(bot, 'session.get', (client) => client.session.get({ sessionID: sessionId }))
|
|
@@ -672,7 +809,7 @@ export async function fork(
|
|
|
672
809
|
return new ConfigError({ reason: "This session is not the thread's root or a direct subagent. Run /fork or /fork-subagent in the session's own thread." })
|
|
673
810
|
}
|
|
674
811
|
}
|
|
675
|
-
const project = await
|
|
812
|
+
const project = await conversationProject(bot, source)
|
|
676
813
|
if (project instanceof Error) return project
|
|
677
814
|
const channel = await textChannel(bot, project.channelId)
|
|
678
815
|
if (channel instanceof Error) return channel
|
|
@@ -710,13 +847,13 @@ export async function fork(
|
|
|
710
847
|
}
|
|
711
848
|
const intro = subagent
|
|
712
849
|
? `**Forked subagent session created!**\nAgent: \`${subagent.agent}\`\nTask: ${subagent.description || 'No description'}\nFrom: \`${sessionId}\`\nNew session: \`${forked.id}\``
|
|
713
|
-
: `**Forked session created!**\nFrom: <#${
|
|
850
|
+
: `**Forked session created!**\nFrom: <#${source.id}> (\`${sessionId}\`)\nNew session: \`${forked.id}\``
|
|
714
851
|
return adoptSession(bot, {
|
|
715
852
|
channel,
|
|
716
853
|
session: relocated,
|
|
717
854
|
discard,
|
|
718
855
|
// OpenCode titles forks "<title> (fork #1)".
|
|
719
|
-
threadName: name ?? (forked.title || `Fork: ${subagent?.description ||
|
|
856
|
+
threadName: name ?? (forked.title || `Fork: ${subagent?.description || source.name}`),
|
|
720
857
|
intro,
|
|
721
858
|
note: 'You can now continue the conversation from this point.',
|
|
722
859
|
author,
|
|
@@ -729,31 +866,36 @@ export async function fork(
|
|
|
729
866
|
export async function forkBtw(
|
|
730
867
|
bot: Bot,
|
|
731
868
|
{
|
|
732
|
-
|
|
869
|
+
source,
|
|
733
870
|
text,
|
|
734
871
|
author,
|
|
735
872
|
messageId,
|
|
736
873
|
files = [],
|
|
737
874
|
agent,
|
|
875
|
+
parentSessionId,
|
|
876
|
+
sender,
|
|
738
877
|
}: {
|
|
739
|
-
|
|
878
|
+
source: Conversation
|
|
740
879
|
text: string
|
|
741
880
|
author: Author
|
|
742
881
|
messageId: string
|
|
743
882
|
files?: readonly PromptFile[]
|
|
744
883
|
// A voice message asked for this agent.
|
|
745
884
|
agent?: string
|
|
885
|
+
// Explicit `kimaki send --parent-session`, recorded in the fork's marker.
|
|
886
|
+
parentSessionId?: string
|
|
887
|
+
sender?: Sender
|
|
746
888
|
},
|
|
747
889
|
): Promise<ConfigError | OpenCodeUnavailableError | OpenCodeError | DiscordError | DbError | { threadId: string; sessionId: string }> {
|
|
748
|
-
const
|
|
749
|
-
if (
|
|
750
|
-
const project = await
|
|
890
|
+
const sourceSessionId = rootSession(bot, source.id)
|
|
891
|
+
if (sourceSessionId instanceof Error) return sourceSessionId
|
|
892
|
+
const project = await conversationProject(bot, source)
|
|
751
893
|
if (project instanceof Error) return project
|
|
752
894
|
const channel = await textChannel(bot, project.channelId)
|
|
753
895
|
if (channel instanceof Error) return channel
|
|
754
896
|
|
|
755
897
|
const [forked, thread] = await Promise.all([
|
|
756
|
-
oc(bot, 'session.fork', (client) => client.session.fork({ sessionID:
|
|
898
|
+
oc(bot, 'session.fork', (client) => client.session.fork({ sessionID: sourceSessionId })),
|
|
757
899
|
channel.threads
|
|
758
900
|
.create({ name: `btw: ${text.replace(/\s+/g, ' ')}`.slice(0, 100), autoArchiveDuration: 1440 })
|
|
759
901
|
.catch((cause) => new DiscordError({ operation: 'create btw thread', cause })),
|
|
@@ -769,9 +911,16 @@ export async function forkBtw(
|
|
|
769
911
|
return thread
|
|
770
912
|
}
|
|
771
913
|
|
|
772
|
-
const kimaki = { threadId: thread.id, channelId: project.channelId, source: 'discord', ...cliContext(bot) }
|
|
914
|
+
const kimaki = { threadId: thread.id, channelId: project.channelId, source: 'discord', ...cliContext(bot), ...(parentSessionId && { parentSessionId }) }
|
|
915
|
+
const fromCoordinator = !source.isThread()
|
|
773
916
|
const marked = await oc(bot, 'session.update', (client) => client.session.update({ sessionID: forked.id, metadata: { kimaki } }))
|
|
774
917
|
if (marked instanceof Error) return marked
|
|
918
|
+
// A fork of a thread keeps its copied instructions (prompt cache); a fork of a coordinator gets thread
|
|
919
|
+
// ones. The plugin makes it a worker (plugin/coordinator.ts).
|
|
920
|
+
if (fromCoordinator) {
|
|
921
|
+
const instructions = await putInstructions(bot, { sessionId: forked.id, conversation: thread, channel, directory: project.directory, userId: author.id, parentSessionId: sourceSessionId })
|
|
922
|
+
if (instructions instanceof Error) return instructions
|
|
923
|
+
}
|
|
775
924
|
const bound = await bindThread(bot, {
|
|
776
925
|
threadId: thread.id,
|
|
777
926
|
sessionId: forked.id,
|
|
@@ -782,7 +931,7 @@ export async function forkBtw(
|
|
|
782
931
|
if (bound instanceof Error) return bound
|
|
783
932
|
await thread.members.add(author.id).catch((e: Error) => logger.warn(`add btw member`, e))
|
|
784
933
|
const intro = await thread
|
|
785
|
-
.send({ content: `Reusing context from <#${
|
|
934
|
+
.send({ content: `Reusing context from <#${source.id}> to answer prompt...\n${text}`.slice(0, 2_000), allowedMentions: { parse: [] }, flags: SILENT_MESSAGE_FLAGS })
|
|
786
935
|
.catch((cause) => new DiscordError({ operation: 'send btw intro', cause }))
|
|
787
936
|
if (intro instanceof Error) return intro
|
|
788
937
|
if (agent) {
|
|
@@ -794,11 +943,12 @@ export async function forkBtw(
|
|
|
794
943
|
sessionId: forked.id,
|
|
795
944
|
threadId: thread.id,
|
|
796
945
|
threadName: thread.name,
|
|
797
|
-
text: btwPrompt({ text, parentSessionId, sourceThreadId:
|
|
946
|
+
text: btwPrompt({ text, parentSessionId: sourceSessionId, sourceThreadId: source.id, sessionId: forked.id, threadId: thread.id }),
|
|
798
947
|
author,
|
|
799
948
|
messageId,
|
|
800
949
|
delivery: 'steer',
|
|
801
950
|
files,
|
|
951
|
+
sender,
|
|
802
952
|
})
|
|
803
953
|
if (sent instanceof Error) return sent
|
|
804
954
|
return { threadId: thread.id, sessionId: forked.id }
|
package/src/shell.e2e.test.ts
CHANGED
|
@@ -95,12 +95,12 @@ test('!cmd while busy runs at once, does not interrupt, and its output reaches t
|
|
|
95
95
|
|
|
96
96
|
slow-done
|
|
97
97
|
ok
|
|
98
|
-
-# *
|
|
98
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*
|
|
99
99
|
--- from: user (tommy)
|
|
100
100
|
What did it print? shell-check
|
|
101
101
|
--- from: assistant (TestBot)
|
|
102
102
|
I saw your shell output
|
|
103
|
-
-# *
|
|
103
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
104
104
|
`)
|
|
105
105
|
})
|
|
106
106
|
|
|
@@ -163,11 +163,11 @@ test('/command args runs an OpenCode command; unknown names are plain text', asy
|
|
|
163
163
|
--- from: assistant (TestBot)
|
|
164
164
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
165
165
|
reviewed
|
|
166
|
-
-# *
|
|
166
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
167
167
|
--- from: user (tommy)
|
|
168
168
|
/nope unknown-marker
|
|
169
169
|
--- from: assistant (TestBot)
|
|
170
170
|
plain text
|
|
171
|
-
-# *
|
|
171
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
172
172
|
`)
|
|
173
173
|
})
|
|
@@ -47,6 +47,7 @@ test('catalog names are sanitized, keep their suffix, skip collisions and MCP pr
|
|
|
47
47
|
"fork-subagent",
|
|
48
48
|
"btw",
|
|
49
49
|
"abort",
|
|
50
|
+
"reset-coordinator",
|
|
50
51
|
"queue",
|
|
51
52
|
"clear-queue",
|
|
52
53
|
"queue-command",
|
|
@@ -60,6 +61,7 @@ test('catalog names are sanitized, keep their suffix, skip collisions and MCP pr
|
|
|
60
61
|
"model",
|
|
61
62
|
"model-variant",
|
|
62
63
|
"verbosity",
|
|
64
|
+
"toggle-coordinator",
|
|
63
65
|
"routines",
|
|
64
66
|
"command",
|
|
65
67
|
"skill",
|