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/dist/sessions.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
7
7
|
import * as orm from 'drizzle-orm';
|
|
8
|
-
import { cliContext, globalModel,
|
|
8
|
+
import { cliContext, globalModel, oc, parseModel, projectChannelOf, projectOf, readMarker, rootSession, routeText, sessionDirectory, textChannel, } from "./bot.js";
|
|
9
9
|
import { ConfigError, DbError, DiscordError, OpenCodeError, OpenCodeUnavailableError } from "./errors.js";
|
|
10
10
|
import { SILENT_MESSAGE_FLAGS } from "./format-parts.js";
|
|
11
11
|
import { createLogger } from "./logger.js";
|
|
@@ -34,17 +34,17 @@ export function btwPrompt({ text, parentSessionId, sourceThreadId, sessionId, th
|
|
|
34
34
|
text,
|
|
35
35
|
].join('\n');
|
|
36
36
|
}
|
|
37
|
-
// Project channel, project directory and current cwd of a session
|
|
38
|
-
export async function
|
|
39
|
-
const channelId =
|
|
37
|
+
// Project channel, project directory and current cwd of a conversation's root session.
|
|
38
|
+
export async function conversationProject(bot, conversation) {
|
|
39
|
+
const channelId = projectChannelOf(conversation);
|
|
40
40
|
if (!channelId)
|
|
41
|
-
return new DiscordError({ operation: `find the channel of
|
|
41
|
+
return new DiscordError({ operation: `find the channel of conversation ${conversation.id}` });
|
|
42
42
|
const row = await projectOf(bot, channelId);
|
|
43
43
|
if (row instanceof Error)
|
|
44
44
|
return row;
|
|
45
45
|
if (!row)
|
|
46
46
|
return new DiscordError({ operation: `find the project of channel ${channelId}` });
|
|
47
|
-
const sessionId = rootSession(bot,
|
|
47
|
+
const sessionId = rootSession(bot, conversation.id);
|
|
48
48
|
if (sessionId instanceof Error)
|
|
49
49
|
return sessionId;
|
|
50
50
|
const directory = await sessionDirectory(bot, sessionId);
|
|
@@ -54,8 +54,8 @@ export async function threadProject(bot, thread) {
|
|
|
54
54
|
}
|
|
55
55
|
// /cwd and `kimaki session cwd`: show the cwd, or move the session to a
|
|
56
56
|
// project subfolder or linked worktree.
|
|
57
|
-
export async function sessionCwd(bot, {
|
|
58
|
-
const project = await
|
|
57
|
+
export async function sessionCwd(bot, { conversation, directory }) {
|
|
58
|
+
const project = await conversationProject(bot, conversation);
|
|
59
59
|
if (project instanceof Error)
|
|
60
60
|
return project;
|
|
61
61
|
if (!directory)
|
|
@@ -69,7 +69,7 @@ export async function sessionCwd(bot, { thread, directory }) {
|
|
|
69
69
|
const plugin = await bot.features.waitForPlugin(destination);
|
|
70
70
|
if (plugin instanceof Error)
|
|
71
71
|
return plugin;
|
|
72
|
-
const sessionId = rootSession(bot,
|
|
72
|
+
const sessionId = rootSession(bot, conversation.id);
|
|
73
73
|
if (sessionId instanceof Error)
|
|
74
74
|
return sessionId;
|
|
75
75
|
const moved = await oc(bot, 'session.move', (client) => client.session.move({ sessionID: sessionId, directory: destination, delivery: 'steer' }));
|
|
@@ -95,7 +95,8 @@ export async function catalogReady(bot, directory) {
|
|
|
95
95
|
if (ready instanceof Error)
|
|
96
96
|
return ready;
|
|
97
97
|
}
|
|
98
|
-
// Agents a session may switch to (not subagents, not hidden ones).
|
|
98
|
+
// Agents a session may switch to (not subagents, not hidden ones). OpenCode lists
|
|
99
|
+
// its default agent (`default_agent`, else build) first: core/src/agent.ts `list`.
|
|
99
100
|
export async function primaryAgents(bot, directory) {
|
|
100
101
|
const ready = await catalogReady(bot, directory);
|
|
101
102
|
if (ready instanceof Error)
|
|
@@ -105,6 +106,22 @@ export async function primaryAgents(bot, directory) {
|
|
|
105
106
|
return agents;
|
|
106
107
|
return agents.data.filter((agent) => agent.mode !== 'subagent' && !agent.hidden);
|
|
107
108
|
}
|
|
109
|
+
// What a new session of the channel starts with: its project folder, and the channel agent or OpenCode's default agent.
|
|
110
|
+
export async function channelDefaults(bot, channelId) {
|
|
111
|
+
const row = await bot.db.query.channel_directories
|
|
112
|
+
.findFirst({ where: { channel_id: channelId }, with: { channel_agent: true } })
|
|
113
|
+
.catch((cause) => new DbError({ operation: 'read channel defaults', cause }));
|
|
114
|
+
if (row instanceof Error)
|
|
115
|
+
return row;
|
|
116
|
+
if (!row)
|
|
117
|
+
return null;
|
|
118
|
+
if (row.channel_agent)
|
|
119
|
+
return { projectDirectory: row.directory, defaultAgent: row.channel_agent.agent_name };
|
|
120
|
+
const agents = await primaryAgents(bot, row.directory);
|
|
121
|
+
if (agents instanceof Error)
|
|
122
|
+
return agents;
|
|
123
|
+
return { projectDirectory: row.directory, defaultAgent: agents[0]?.id ?? null };
|
|
124
|
+
}
|
|
108
125
|
export async function agentSelection(bot, { directory, agent }) {
|
|
109
126
|
const agents = await primaryAgents(bot, directory);
|
|
110
127
|
if (agents instanceof Error)
|
|
@@ -116,10 +133,17 @@ export async function agentSelection(bot, { directory, agent }) {
|
|
|
116
133
|
return { agent: selected.id, model };
|
|
117
134
|
}
|
|
118
135
|
// OpenCode switches the agent and model separately, including the configured variant.
|
|
119
|
-
export async function switchAgent(bot, { sessionId, directory, agent, model }) {
|
|
136
|
+
export async function switchAgent(bot, { sessionId, directory, agent, model, reselect = false, }) {
|
|
120
137
|
const selected = await agentSelection(bot, { directory, agent });
|
|
121
138
|
if (selected instanceof Error)
|
|
122
139
|
return selected;
|
|
140
|
+
// naming the session's current agent again (agents pass --agent <current_agent>
|
|
141
|
+
// on every send) keeps the model the session runs on, e.g. one set with /model.
|
|
142
|
+
if (!model && !reselect) {
|
|
143
|
+
const info = await oc(bot, 'session.get', (client) => client.session.get({ sessionID: sessionId }));
|
|
144
|
+
if (!(info instanceof Error) && info.agent === selected.agent)
|
|
145
|
+
return { ...selected, model: null };
|
|
146
|
+
}
|
|
123
147
|
const choice = model ?? selected.model;
|
|
124
148
|
if (choice) {
|
|
125
149
|
const switched = await oc(bot, 'session.switchModel', (client) => client.session.switchModel({
|
|
@@ -135,8 +159,8 @@ export async function switchAgent(bot, { sessionId, directory, agent, model }) {
|
|
|
135
159
|
return { ...selected, model: choice };
|
|
136
160
|
}
|
|
137
161
|
// The one durable system instruction of a session (spec 5.4).
|
|
138
|
-
async function putInstructions(bot, { sessionId,
|
|
139
|
-
// The agent list is optional prompt context: a failure must not strand the
|
|
162
|
+
async function putInstructions(bot, { sessionId, conversation, channel, directory, userId, parentSessionId = null, scheduledTask = null, }) {
|
|
163
|
+
// The agent list is optional prompt context: a failure must not strand the conversation.
|
|
140
164
|
const found = await primaryAgents(bot, directory);
|
|
141
165
|
if (found instanceof Error)
|
|
142
166
|
logger.warn(`agent list for instructions failed`, found);
|
|
@@ -144,7 +168,7 @@ async function putInstructions(bot, { sessionId, thread, channel, directory, use
|
|
|
144
168
|
const agents = found instanceof Error ? [] : found.map((agent) => ({ name: agent.id, description: agent.description ?? '' }));
|
|
145
169
|
const value = baseInstructions({
|
|
146
170
|
sessionId,
|
|
147
|
-
threadId:
|
|
171
|
+
threadId: conversation.id,
|
|
148
172
|
channelId: channel.id,
|
|
149
173
|
guildId: channel.guildId,
|
|
150
174
|
userId,
|
|
@@ -153,6 +177,9 @@ async function putInstructions(bot, { sessionId, thread, channel, directory, use
|
|
|
153
177
|
agents,
|
|
154
178
|
parentSessionId,
|
|
155
179
|
scheduledTask,
|
|
180
|
+
// Derived from where the session lives, never from bindings that can be stale.
|
|
181
|
+
coordinator: conversation.id === channel.id,
|
|
182
|
+
disabled: bot.disabledPrompt,
|
|
156
183
|
});
|
|
157
184
|
const put = await oc(bot, 'instructions.entry.put', (client) => client.session.instructions.entry.put({ sessionID: sessionId, key: INSTRUCTION_KEY, value }));
|
|
158
185
|
if (put instanceof Error)
|
|
@@ -171,6 +198,7 @@ export function createPluginWait(bot) {
|
|
|
171
198
|
return;
|
|
172
199
|
const location = { directory };
|
|
173
200
|
const deadline = Date.now() + 10_000;
|
|
201
|
+
let loadError;
|
|
174
202
|
while (Date.now() < deadline) {
|
|
175
203
|
const endpoint = bot.opencode.endpoint;
|
|
176
204
|
if (!endpoint)
|
|
@@ -190,8 +218,18 @@ export function createPluginWait(bot) {
|
|
|
190
218
|
active.set(endpoint, (active.get(endpoint) ?? new Set()).add(directory));
|
|
191
219
|
return;
|
|
192
220
|
}
|
|
221
|
+
// A plugin that fails to load has no ID: find the shim directory by its path.
|
|
222
|
+
const failed = plugins.data.find((plugin) => plugin.source.type === 'local' && path.basename(plugin.source.path) === 'kimaki');
|
|
223
|
+
loadError = failed?.state.status === 'failed' ? (failed.state.ref ?? failed.state.error) : undefined;
|
|
193
224
|
await sleep(200);
|
|
194
225
|
}
|
|
226
|
+
if (loadError) {
|
|
227
|
+
// TODO: remove the restart hint once OpenCode's Bun evicts failed ESM loads; Bun 1.4.4 keeps
|
|
228
|
+
// them forever, so a reload during `pnpm build` stays broken after the build ends.
|
|
229
|
+
return new ConfigError({
|
|
230
|
+
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\``,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
195
233
|
return new ConfigError({
|
|
196
234
|
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`,
|
|
197
235
|
});
|
|
@@ -202,7 +240,7 @@ export function createPluginWait(bot) {
|
|
|
202
240
|
// entry; sessions of an older bot run point at its old lock port. Checked
|
|
203
241
|
// on each input instead of for every binding at startup (a V1 install has
|
|
204
242
|
// thousands of bindings).
|
|
205
|
-
export async function ensureSessionMarker(bot, { sessionId,
|
|
243
|
+
export async function ensureSessionMarker(bot, { sessionId, conversation, channelId, directory, userId, }) {
|
|
206
244
|
const plugin = await bot.features.waitForPlugin(directory);
|
|
207
245
|
if (plugin instanceof Error)
|
|
208
246
|
return plugin;
|
|
@@ -210,19 +248,19 @@ export async function ensureSessionMarker(bot, { sessionId, thread, channelId, d
|
|
|
210
248
|
if (info instanceof Error)
|
|
211
249
|
return info;
|
|
212
250
|
const marker = readMarker(info.metadata);
|
|
213
|
-
if (marker &&
|
|
251
|
+
if (marker && marker.dataDir === bot.dataDir && marker.lockPort === bot.lockPort)
|
|
214
252
|
return;
|
|
215
253
|
// Instructions first: the marker means "set up", so a failure here is retried on the next input.
|
|
216
254
|
if (!marker) {
|
|
217
|
-
logger.log(`adopting legacy session ${sessionId} of
|
|
255
|
+
logger.log(`adopting legacy session ${sessionId} of conversation ${conversation.id}`);
|
|
218
256
|
const channel = await textChannel(bot, channelId);
|
|
219
257
|
if (channel instanceof Error)
|
|
220
258
|
return channel;
|
|
221
|
-
const instructions = await putInstructions(bot, { sessionId,
|
|
259
|
+
const instructions = await putInstructions(bot, { sessionId, conversation, channel, directory, userId });
|
|
222
260
|
if (instructions instanceof Error)
|
|
223
261
|
return instructions;
|
|
224
262
|
}
|
|
225
|
-
const kimaki = { ...marker?.fields, source: 'discord', ...cliContext(bot), threadId:
|
|
263
|
+
const kimaki = { ...marker?.fields, source: 'discord', ...cliContext(bot), threadId: conversation.id, channelId };
|
|
226
264
|
const updated = await oc(bot, 'session.update', (client) => client.session.update({ sessionID: sessionId, metadata: { ...info.metadata, kimaki } }));
|
|
227
265
|
if (updated instanceof Error)
|
|
228
266
|
return updated;
|
|
@@ -243,7 +281,7 @@ export async function refreshCliContext(bot) {
|
|
|
243
281
|
const marker = readMarker(info.metadata);
|
|
244
282
|
if (!marker)
|
|
245
283
|
continue;
|
|
246
|
-
if (
|
|
284
|
+
if (marker.dataDir === bot.dataDir && marker.lockPort === bot.lockPort)
|
|
247
285
|
continue;
|
|
248
286
|
const metadata = { ...info.metadata, kimaki: { ...marker.fields, ...cliContext(bot) } };
|
|
249
287
|
const result = await oc(bot, 'refresh Kimaki CLI context', (client) => client.session.update({ sessionID: sessionId, metadata }));
|
|
@@ -251,7 +289,24 @@ export async function refreshCliContext(bot) {
|
|
|
251
289
|
return result;
|
|
252
290
|
}
|
|
253
291
|
}
|
|
254
|
-
|
|
292
|
+
// Agent and model of a new session: an agent the input asked for, else the
|
|
293
|
+
// channel agent; the model from the input, that agent, the channel, the global default.
|
|
294
|
+
async function agentAndModel(bot, { defaults, route, directory, explicitModel }) {
|
|
295
|
+
const requestedAgent = (route.kind === 'steer' && route.agent) || defaults?.channel_agent?.agent_name;
|
|
296
|
+
const selected = requestedAgent ? await agentSelection(bot, { directory, agent: requestedAgent }) : null;
|
|
297
|
+
if (selected instanceof Error)
|
|
298
|
+
return selected;
|
|
299
|
+
const agentModel = route.kind === 'steer' && route.agent ? selected?.model : null;
|
|
300
|
+
const global = await globalModel(bot);
|
|
301
|
+
if (global instanceof Error)
|
|
302
|
+
return global;
|
|
303
|
+
const choice = explicitModel ?? agentModel ?? parseModel(defaults?.channel_model?.model_id, defaults?.channel_model?.variant) ?? selected?.model ?? global;
|
|
304
|
+
const model = choice
|
|
305
|
+
? { providerID: choice.providerID, id: choice.id, ...(choice.variant && { variant: choice.variant }) }
|
|
306
|
+
: undefined;
|
|
307
|
+
return { model, agent: selected?.agent };
|
|
308
|
+
}
|
|
309
|
+
export async function startSession(bot, { channelId, directory: requestedDirectory, route, author, members = [], messageId, startMessageId = messageId, showInput = startMessageId === null, threadName: explicitName, files = [], model: explicitModel, permissions, parentSessionId, sender, task, worktree, baseBranch, run = true, }) {
|
|
255
310
|
const defaults = await bot.db.query.channel_directories
|
|
256
311
|
.findFirst({ where: { channel_id: channelId }, with: { channel_model: true, channel_agent: true, channel_worktree: true } })
|
|
257
312
|
.catch((cause) => new DbError({ operation: 'read channel defaults', cause }));
|
|
@@ -262,7 +317,7 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
|
|
|
262
317
|
const useWorktree = typeof worktree === 'string' || (worktree !== false && channelWorktrees);
|
|
263
318
|
const textSlug = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 40).replace(/^-|-$/g, '') || 'session';
|
|
264
319
|
const slug = typeof worktree === 'string' && worktree ? worktree : `${textSlug}-${crypto.randomBytes(4).toString('hex')}`;
|
|
265
|
-
const threadName = explicitName ?? (useWorktree ?
|
|
320
|
+
const threadName = explicitName ?? (useWorktree ? slug : text.replace(/\s+/g, ' ').slice(0, 80) || 'Kimaki session');
|
|
266
321
|
// The thread shows in Discord while the checkout, plugin, agent and model are resolved.
|
|
267
322
|
const creating = (async () => {
|
|
268
323
|
const channel = await textChannel(bot, channelId);
|
|
@@ -273,9 +328,12 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
|
|
|
273
328
|
.catch((cause) => new DiscordError({ operation: 'create thread', cause }));
|
|
274
329
|
if (thread instanceof Error)
|
|
275
330
|
return thread;
|
|
276
|
-
// Discord only adds the thread creator (the bot);
|
|
277
|
-
|
|
278
|
-
|
|
331
|
+
// Discord only adds the thread creator (the bot); users must be added to see it in the sidebar.
|
|
332
|
+
// Not awaited: the session does not need them, and each add is a Discord round trip.
|
|
333
|
+
for (const id of new Set([author.id, ...members])) {
|
|
334
|
+
if (id !== bot.discord.user?.id)
|
|
335
|
+
void thread.members.add(id).catch((e) => logger.warn(`add member ${id}`, e));
|
|
336
|
+
}
|
|
279
337
|
return { channel, thread };
|
|
280
338
|
})();
|
|
281
339
|
const setup = await (async () => {
|
|
@@ -288,19 +346,10 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
|
|
|
288
346
|
const plugin = await bot.features.waitForPlugin(directory);
|
|
289
347
|
if (plugin instanceof Error)
|
|
290
348
|
return plugin;
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const agentModel = route.kind === 'steer' && route.agent ? selected?.model : null;
|
|
296
|
-
const global = await globalModel(bot);
|
|
297
|
-
if (global instanceof Error)
|
|
298
|
-
return global;
|
|
299
|
-
const choice = explicitModel ?? agentModel ?? parseModel(defaults?.channel_model?.model_id, defaults?.channel_model?.variant) ?? selected?.model ?? global;
|
|
300
|
-
const model = choice
|
|
301
|
-
? { providerID: choice.providerID, id: choice.id, ...(choice.variant && { variant: choice.variant }) }
|
|
302
|
-
: undefined;
|
|
303
|
-
return { checkout, directory, model, agent: selected?.agent };
|
|
349
|
+
const chosen = await agentAndModel(bot, { defaults, route, directory, explicitModel });
|
|
350
|
+
if (chosen instanceof Error)
|
|
351
|
+
return chosen;
|
|
352
|
+
return { checkout, directory, ...chosen };
|
|
304
353
|
})();
|
|
305
354
|
const created = await creating;
|
|
306
355
|
if (created instanceof Error)
|
|
@@ -316,6 +365,7 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
|
|
|
316
365
|
channelId,
|
|
317
366
|
source: task ? 'task' : 'discord',
|
|
318
367
|
...(task && { taskId: task.id, task }),
|
|
368
|
+
...(bot.warmupTimers && { warmup: bot.warmupTimers }),
|
|
319
369
|
...cliContext(bot),
|
|
320
370
|
...(parentSessionId && { parentSessionId }),
|
|
321
371
|
};
|
|
@@ -332,7 +382,7 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
|
|
|
332
382
|
bot.analytics.track('session_created', { has_worktree: Boolean(checkout), source: 'discord' });
|
|
333
383
|
const instructions = await putInstructions(bot, {
|
|
334
384
|
sessionId: session.id,
|
|
335
|
-
thread,
|
|
385
|
+
conversation: thread,
|
|
336
386
|
channel,
|
|
337
387
|
directory,
|
|
338
388
|
userId: author.id,
|
|
@@ -345,9 +395,10 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
|
|
|
345
395
|
if (bound instanceof Error)
|
|
346
396
|
return bound;
|
|
347
397
|
if (!run) {
|
|
398
|
+
const ready = checkout ? `Worktree ready: \`${directory}\`\nBranch: \`${checkout.branch ?? 'detached'}\`\n` : '';
|
|
348
399
|
const posted = await thread
|
|
349
400
|
.send({
|
|
350
|
-
content:
|
|
401
|
+
content: `${ready}Send a message to start working.`,
|
|
351
402
|
allowedMentions: { parse: [] },
|
|
352
403
|
flags: SILENT_MESSAGE_FLAGS,
|
|
353
404
|
})
|
|
@@ -359,26 +410,78 @@ export async function startSession(bot, { channelId, directory: requestedDirecto
|
|
|
359
410
|
if (showInput) {
|
|
360
411
|
const names = files.map((file) => file.name).join(', ');
|
|
361
412
|
const shown = names ? `${text}\nFiles: ${names}` : text;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (echo instanceof Error)
|
|
366
|
-
return echo;
|
|
413
|
+
// Through the thread's effect queue, so the prompt does not wait for Discord,
|
|
414
|
+
// the echo comes before the session output and a long prompt is split.
|
|
415
|
+
bot.effects.run(thread.id, [{ type: 'send', text: formatEcho({ username: author.username, text: shown }), echo: true }]);
|
|
367
416
|
}
|
|
368
|
-
const
|
|
417
|
+
const base = { sessionId: session.id, threadId: thread.id, threadName, author, messageId, files, sender };
|
|
418
|
+
// A new session has no run, question or permission to cancel: prompt directly instead of steer().
|
|
419
|
+
const sent = route.kind === 'steer'
|
|
420
|
+
? await prompt(bot, { ...base, text: route.text, delivery: 'steer' })
|
|
421
|
+
: await runInSession(bot, { ...base, directory, route });
|
|
422
|
+
if (sent instanceof Error)
|
|
423
|
+
return sent;
|
|
424
|
+
return { threadId: thread.id, sessionId: session.id };
|
|
425
|
+
}
|
|
426
|
+
// The first message of a coordinator channel: one long session that lives in the
|
|
427
|
+
// channel itself, bound like a thread with the channel ID as its conversation.
|
|
428
|
+
export async function startCoordinator(bot, { channel, route, author, messageId, files = [], }) {
|
|
429
|
+
const defaults = await bot.db.query.channel_directories
|
|
430
|
+
.findFirst({ where: { channel_id: channel.id }, with: { channel_model: true, channel_agent: true } })
|
|
431
|
+
.catch((cause) => new DbError({ operation: 'read channel defaults', cause }));
|
|
432
|
+
if (defaults instanceof Error)
|
|
433
|
+
return defaults;
|
|
434
|
+
if (!defaults)
|
|
435
|
+
return new ConfigError({ reason: 'This channel is not configured with a project directory' });
|
|
436
|
+
const directory = defaults.directory;
|
|
437
|
+
const plugin = await bot.features.waitForPlugin(directory);
|
|
438
|
+
if (plugin instanceof Error)
|
|
439
|
+
return plugin;
|
|
440
|
+
const chosen = await agentAndModel(bot, { defaults, route, directory });
|
|
441
|
+
if (chosen instanceof Error)
|
|
442
|
+
return chosen;
|
|
443
|
+
const title = `coordinator: ${channel.name}`;
|
|
444
|
+
const session = await oc(bot, 'session.create', (client) => client.session.create({
|
|
445
|
+
title,
|
|
446
|
+
location: { directory },
|
|
447
|
+
...(chosen.model && { model: chosen.model }),
|
|
448
|
+
...(chosen.agent && { agent: chosen.agent }),
|
|
449
|
+
permissions: [NO_QUESTION_TOOL, ...disabledSkillRules(bot.disabledSkills)],
|
|
450
|
+
metadata: { kimaki: { threadId: channel.id, channelId: channel.id, source: 'discord', ...(bot.warmupTimers && { warmup: bot.warmupTimers }), ...cliContext(bot) } },
|
|
451
|
+
}));
|
|
452
|
+
if (session instanceof Error)
|
|
453
|
+
return session;
|
|
454
|
+
bot.analytics.track('session_created', { has_worktree: false, source: 'discord' });
|
|
455
|
+
const instructions = await putInstructions(bot, { sessionId: session.id, conversation: channel, channel, directory, userId: author.id });
|
|
456
|
+
if (instructions instanceof Error)
|
|
457
|
+
return instructions;
|
|
458
|
+
const bound = await bindThread(bot, { threadId: channel.id, sessionId: session.id, channelId: channel.id, directory, isNew: true });
|
|
459
|
+
if (bound instanceof Error)
|
|
460
|
+
return bound;
|
|
369
461
|
const sent = await runInSession(bot, {
|
|
370
462
|
sessionId: session.id,
|
|
371
|
-
threadId:
|
|
372
|
-
threadName,
|
|
463
|
+
threadId: channel.id,
|
|
464
|
+
threadName: channel.name,
|
|
373
465
|
directory,
|
|
374
|
-
route
|
|
466
|
+
route,
|
|
375
467
|
author,
|
|
376
468
|
messageId,
|
|
377
469
|
files,
|
|
378
470
|
});
|
|
379
471
|
if (sent instanceof Error)
|
|
380
472
|
return sent;
|
|
381
|
-
return { threadId:
|
|
473
|
+
return { threadId: channel.id, sessionId: session.id };
|
|
474
|
+
}
|
|
475
|
+
// /reset-coordinator: the channel forgets its coordinator. The next message starts a fresh
|
|
476
|
+
// one; the old session stays in OpenCode for /resume.
|
|
477
|
+
export async function endCoordinator(bot, channelId) {
|
|
478
|
+
const deleted = await bot.db
|
|
479
|
+
.delete(schema.thread_sessions)
|
|
480
|
+
.where(orm.eq(schema.thread_sessions.thread_id, channelId))
|
|
481
|
+
.catch((cause) => new DbError({ operation: 'delete coordinator binding', cause }));
|
|
482
|
+
if (deleted instanceof Error)
|
|
483
|
+
return deleted;
|
|
484
|
+
bot.eventLoop.unbind(channelId);
|
|
382
485
|
}
|
|
383
486
|
// --- Sessions that get a new thread: /resume, /fork, /fork-subagent, btw.
|
|
384
487
|
// Binds an existing session to a new thread. Order matters:
|
|
@@ -415,7 +518,7 @@ async function adoptSession(bot, { channel, session, threadName, intro, note, au
|
|
|
415
518
|
return marked;
|
|
416
519
|
const instructions = await putInstructions(bot, {
|
|
417
520
|
sessionId,
|
|
418
|
-
thread,
|
|
521
|
+
conversation: thread,
|
|
419
522
|
channel,
|
|
420
523
|
directory: session.location.directory,
|
|
421
524
|
userId: author.id,
|
|
@@ -519,8 +622,8 @@ export async function resume(bot, { channelId, sessionId, author }) {
|
|
|
519
622
|
}
|
|
520
623
|
// Forks the thread's session (or one of its subagent sessions) into a new
|
|
521
624
|
// thread. `before`: a user message ID; the fork ends right before it.
|
|
522
|
-
export async function fork(bot, {
|
|
523
|
-
const root = rootSession(bot,
|
|
625
|
+
export async function fork(bot, { source, sessionId, before, subagent, name, author, worktree, }) {
|
|
626
|
+
const root = rootSession(bot, source.id);
|
|
524
627
|
if (root instanceof Error)
|
|
525
628
|
return root;
|
|
526
629
|
if (sessionId !== root) {
|
|
@@ -531,7 +634,7 @@ export async function fork(bot, { sourceThread, sessionId, before, subagent, nam
|
|
|
531
634
|
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." });
|
|
532
635
|
}
|
|
533
636
|
}
|
|
534
|
-
const project = await
|
|
637
|
+
const project = await conversationProject(bot, source);
|
|
535
638
|
if (project instanceof Error)
|
|
536
639
|
return project;
|
|
537
640
|
const channel = await textChannel(bot, project.channelId);
|
|
@@ -579,13 +682,13 @@ export async function fork(bot, { sourceThread, sessionId, before, subagent, nam
|
|
|
579
682
|
}
|
|
580
683
|
const intro = subagent
|
|
581
684
|
? `**Forked subagent session created!**\nAgent: \`${subagent.agent}\`\nTask: ${subagent.description || 'No description'}\nFrom: \`${sessionId}\`\nNew session: \`${forked.id}\``
|
|
582
|
-
: `**Forked session created!**\nFrom: <#${
|
|
685
|
+
: `**Forked session created!**\nFrom: <#${source.id}> (\`${sessionId}\`)\nNew session: \`${forked.id}\``;
|
|
583
686
|
return adoptSession(bot, {
|
|
584
687
|
channel,
|
|
585
688
|
session: relocated,
|
|
586
689
|
discard,
|
|
587
690
|
// OpenCode titles forks "<title> (fork #1)".
|
|
588
|
-
threadName: name ?? (forked.title || `Fork: ${subagent?.description ||
|
|
691
|
+
threadName: name ?? (forked.title || `Fork: ${subagent?.description || source.name}`),
|
|
589
692
|
intro,
|
|
590
693
|
note: 'You can now continue the conversation from this point.',
|
|
591
694
|
author,
|
|
@@ -594,18 +697,18 @@ export async function fork(bot, { sourceThread, sessionId, before, subagent, nam
|
|
|
594
697
|
// Fork the whole session into a new "btw:" thread that answers one side
|
|
595
698
|
// question (V1 /btw). The source session keeps running. The fork inherits
|
|
596
699
|
// messages, agent, model and instructions, so its prompt cache is warm.
|
|
597
|
-
export async function forkBtw(bot, {
|
|
598
|
-
const
|
|
599
|
-
if (
|
|
600
|
-
return
|
|
601
|
-
const project = await
|
|
700
|
+
export async function forkBtw(bot, { source, text, author, messageId, files = [], agent, parentSessionId, sender, }) {
|
|
701
|
+
const sourceSessionId = rootSession(bot, source.id);
|
|
702
|
+
if (sourceSessionId instanceof Error)
|
|
703
|
+
return sourceSessionId;
|
|
704
|
+
const project = await conversationProject(bot, source);
|
|
602
705
|
if (project instanceof Error)
|
|
603
706
|
return project;
|
|
604
707
|
const channel = await textChannel(bot, project.channelId);
|
|
605
708
|
if (channel instanceof Error)
|
|
606
709
|
return channel;
|
|
607
710
|
const [forked, thread] = await Promise.all([
|
|
608
|
-
oc(bot, 'session.fork', (client) => client.session.fork({ sessionID:
|
|
711
|
+
oc(bot, 'session.fork', (client) => client.session.fork({ sessionID: sourceSessionId })),
|
|
609
712
|
channel.threads
|
|
610
713
|
.create({ name: `btw: ${text.replace(/\s+/g, ' ')}`.slice(0, 100), autoArchiveDuration: 1440 })
|
|
611
714
|
.catch((cause) => new DiscordError({ operation: 'create btw thread', cause })),
|
|
@@ -622,10 +725,18 @@ export async function forkBtw(bot, { sourceThread, text, author, messageId, file
|
|
|
622
725
|
logger.warn(`discard btw fork ${forked.id}`, removed);
|
|
623
726
|
return thread;
|
|
624
727
|
}
|
|
625
|
-
const kimaki = { threadId: thread.id, channelId: project.channelId, source: 'discord', ...cliContext(bot) };
|
|
728
|
+
const kimaki = { threadId: thread.id, channelId: project.channelId, source: 'discord', ...cliContext(bot), ...(parentSessionId && { parentSessionId }) };
|
|
729
|
+
const fromCoordinator = !source.isThread();
|
|
626
730
|
const marked = await oc(bot, 'session.update', (client) => client.session.update({ sessionID: forked.id, metadata: { kimaki } }));
|
|
627
731
|
if (marked instanceof Error)
|
|
628
732
|
return marked;
|
|
733
|
+
// A fork of a thread keeps its copied instructions (prompt cache); a fork of a coordinator gets thread
|
|
734
|
+
// ones. The plugin makes it a worker (plugin/coordinator.ts).
|
|
735
|
+
if (fromCoordinator) {
|
|
736
|
+
const instructions = await putInstructions(bot, { sessionId: forked.id, conversation: thread, channel, directory: project.directory, userId: author.id, parentSessionId: sourceSessionId });
|
|
737
|
+
if (instructions instanceof Error)
|
|
738
|
+
return instructions;
|
|
739
|
+
}
|
|
629
740
|
const bound = await bindThread(bot, {
|
|
630
741
|
threadId: thread.id,
|
|
631
742
|
sessionId: forked.id,
|
|
@@ -637,7 +748,7 @@ export async function forkBtw(bot, { sourceThread, text, author, messageId, file
|
|
|
637
748
|
return bound;
|
|
638
749
|
await thread.members.add(author.id).catch((e) => logger.warn(`add btw member`, e));
|
|
639
750
|
const intro = await thread
|
|
640
|
-
.send({ content: `Reusing context from <#${
|
|
751
|
+
.send({ content: `Reusing context from <#${source.id}> to answer prompt...\n${text}`.slice(0, 2_000), allowedMentions: { parse: [] }, flags: SILENT_MESSAGE_FLAGS })
|
|
641
752
|
.catch((cause) => new DiscordError({ operation: 'send btw intro', cause }));
|
|
642
753
|
if (intro instanceof Error)
|
|
643
754
|
return intro;
|
|
@@ -650,11 +761,12 @@ export async function forkBtw(bot, { sourceThread, text, author, messageId, file
|
|
|
650
761
|
sessionId: forked.id,
|
|
651
762
|
threadId: thread.id,
|
|
652
763
|
threadName: thread.name,
|
|
653
|
-
text: btwPrompt({ text, parentSessionId, sourceThreadId:
|
|
764
|
+
text: btwPrompt({ text, parentSessionId: sourceSessionId, sourceThreadId: source.id, sessionId: forked.id, threadId: thread.id }),
|
|
654
765
|
author,
|
|
655
766
|
messageId,
|
|
656
767
|
delivery: 'steer',
|
|
657
768
|
files,
|
|
769
|
+
sender,
|
|
658
770
|
});
|
|
659
771
|
if (sent instanceof Error)
|
|
660
772
|
return sent;
|