kimaki 0.30.1 → 0.31.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/dist/agent-model.e2e.test.js +27 -19
- package/dist/analytics.js +3 -1
- package/dist/btw-prefix-detection.js +9 -0
- package/dist/btw-prefix-detection.test.js +38 -1
- package/dist/cli-commands/send.js +2 -2
- package/dist/cli-runner.js +22 -10
- package/dist/cli-runner.test.js +34 -1
- package/dist/cli-send-thread.e2e.test.js +1 -1
- package/dist/commands/btw.js +147 -39
- package/dist/commands/context-usage.js +47 -0
- package/dist/commands/context-usage.test.js +49 -0
- package/dist/commands/fork.js +32 -1
- package/dist/commands/model.js +69 -14
- package/dist/commands/new-worktree.js +5 -2
- package/dist/commands/queue.js +32 -25
- package/dist/commands/session.js +7 -2
- package/dist/db.test.js +17 -7
- package/dist/discord-bot.js +65 -42
- package/dist/errors.js +10 -5
- package/dist/external-directory-permissions.e2e.test.js +2 -2
- package/dist/external-directory-project-deny.e2e.test.js +1 -1
- package/dist/gateway-proxy.e2e.test.js +5 -5
- package/dist/message-formatting.js +34 -9
- package/dist/message-formatting.test.js +2 -2
- package/dist/message-preprocessing.js +53 -75
- package/dist/queue-advanced-abort.e2e.test.js +1 -1
- package/dist/queue-advanced-action-buttons.e2e.test.js +3 -3
- package/dist/queue-advanced-footer.e2e.test.js +7 -7
- package/dist/queue-advanced-model-switch.e2e.test.js +2 -2
- package/dist/queue-advanced-permissions-typing.e2e.test.js +1 -1
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +3 -3
- package/dist/queue-delete-message.e2e.test.js +146 -5
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +2 -2
- package/dist/queue-interrupt-drain.e2e.test.js +24 -14
- package/dist/queue-question-select-drain.e2e.test.js +27 -27
- package/dist/runtime-lifecycle.e2e.test.js +6 -6
- package/dist/session-handler/agent-utils.js +16 -17
- package/dist/session-handler/event-stream-state.js +10 -2
- package/dist/session-handler/event-stream-state.test.js +129 -3
- package/dist/session-handler/thread-session-runtime.js +169 -101
- package/dist/sleep.e2e.test.js +4 -4
- package/dist/system-message.js +214 -392
- package/dist/system-message.test.js +204 -380
- package/dist/test-utils.js +32 -6
- package/dist/thread-message-queue.e2e.test.js +131 -59
- package/dist/undo-redo.e2e.test.js +2 -2
- package/dist/voice-handler.js +2 -1
- package/dist/voice-message.e2e.test.js +22 -31
- package/dist/voice-routing-input.e2e.test.js +2 -2
- package/dist/voice-routing-question.e2e.test.js +59 -2
- package/dist/voice.js +306 -206
- package/dist/voice.test.js +161 -13
- package/dist/worktree-lifecycle.e2e.test.js +7 -7
- package/package.json +8 -11
- package/skills/critique/SKILL.md +4 -0
- package/skills/opensrc/SKILL.md +20 -13
- package/skills/playwriter/SKILL.md +1 -1
- package/src/agent-model.e2e.test.ts +28 -19
- package/src/analytics.ts +3 -1
- package/src/btw-prefix-detection.test.ts +39 -1
- package/src/btw-prefix-detection.ts +11 -0
- package/src/cli-commands/send.ts +8 -2
- package/src/cli-runner.test.ts +37 -0
- package/src/cli-runner.ts +26 -10
- package/src/cli-send-thread.e2e.test.ts +1 -1
- package/src/commands/btw.ts +151 -41
- package/src/commands/context-usage.test.ts +57 -0
- package/src/commands/context-usage.ts +59 -0
- package/src/commands/fork.ts +34 -1
- package/src/commands/model.ts +81 -14
- package/src/commands/new-worktree.ts +5 -2
- package/src/commands/queue.ts +41 -25
- package/src/commands/session.ts +10 -2
- package/src/db.test.ts +18 -6
- package/src/discord-bot.ts +67 -51
- package/src/errors.ts +12 -6
- package/src/external-directory-permissions.e2e.test.ts +2 -2
- package/src/external-directory-project-deny.e2e.test.ts +1 -1
- package/src/gateway-proxy.e2e.test.ts +5 -5
- package/src/message-formatting.test.ts +2 -2
- package/src/message-formatting.ts +37 -10
- package/src/message-preprocessing.ts +69 -91
- package/src/queue-advanced-abort.e2e.test.ts +1 -1
- package/src/queue-advanced-action-buttons.e2e.test.ts +3 -3
- package/src/queue-advanced-footer.e2e.test.ts +7 -7
- package/src/queue-advanced-model-switch.e2e.test.ts +2 -2
- package/src/queue-advanced-permissions-typing.e2e.test.ts +1 -1
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +3 -3
- package/src/queue-delete-message.e2e.test.ts +164 -4
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +2 -2
- package/src/queue-interrupt-drain.e2e.test.ts +24 -13
- package/src/queue-question-select-drain.e2e.test.ts +31 -28
- package/src/runtime-lifecycle.e2e.test.ts +6 -6
- package/src/session-handler/agent-utils.ts +25 -20
- package/src/session-handler/event-stream-state.test.ts +129 -2
- package/src/session-handler/event-stream-state.ts +21 -2
- package/src/session-handler/thread-runtime-state.ts +7 -0
- package/src/session-handler/thread-session-runtime.ts +211 -123
- package/src/sleep.e2e.test.ts +4 -4
- package/src/system-message.test.ts +216 -379
- package/src/system-message.ts +253 -389
- package/src/test-utils.ts +55 -12
- package/src/thread-message-queue.e2e.test.ts +135 -62
- package/src/undo-redo.e2e.test.ts +2 -2
- package/src/voice-handler.ts +2 -1
- package/src/voice-message.e2e.test.ts +22 -33
- package/src/voice-routing-input.e2e.test.ts +2 -2
- package/src/voice-routing-question.e2e.test.ts +59 -2
- package/src/voice.test.ts +164 -14
- package/src/voice.ts +400 -279
- package/src/worktree-lifecycle.e2e.test.ts +7 -7
|
@@ -400,7 +400,7 @@ describe('agent model resolution', () => {
|
|
|
400
400
|
Reply with exactly: agent-model-check
|
|
401
401
|
--- from: assistant (TestBot)
|
|
402
402
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
403
|
-
|
|
403
|
+
ok
|
|
404
404
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
405
405
|
`);
|
|
406
406
|
expect(footerMessage).toBeDefined();
|
|
@@ -444,7 +444,7 @@ describe('agent model resolution', () => {
|
|
|
444
444
|
Reply with exactly: system-context-check
|
|
445
445
|
--- from: assistant (TestBot)
|
|
446
446
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
447
|
-
|
|
447
|
+
system-context-ok
|
|
448
448
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
449
449
|
`);
|
|
450
450
|
}, 15_000);
|
|
@@ -550,7 +550,7 @@ describe('agent model resolution', () => {
|
|
|
550
550
|
Reply with exactly: channel-model-check
|
|
551
551
|
--- from: assistant (TestBot)
|
|
552
552
|
-# *using deterministic-provider/channel-model-v2*
|
|
553
|
-
|
|
553
|
+
ok
|
|
554
554
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
|
|
555
555
|
`);
|
|
556
556
|
expect(footerMessage).toBeDefined();
|
|
@@ -594,7 +594,7 @@ describe('agent model resolution', () => {
|
|
|
594
594
|
Reply with exactly: variant-check
|
|
595
595
|
--- from: assistant (TestBot)
|
|
596
596
|
-# *using deterministic-provider/channel-model-v2*
|
|
597
|
-
|
|
597
|
+
ok
|
|
598
598
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
|
|
599
599
|
`);
|
|
600
600
|
expect(footer?.content).toContain(CHANNEL_MODEL);
|
|
@@ -655,13 +655,21 @@ describe('agent model resolution', () => {
|
|
|
655
655
|
const forkedSessionModel = forkedSessionId
|
|
656
656
|
? await getSessionModel(forkedSessionId)
|
|
657
657
|
: undefined;
|
|
658
|
+
// The immediate btw ack is edited in place with the fork link.
|
|
659
|
+
const sourceThreadText = (await discord.thread(sourceThread.id).text())
|
|
660
|
+
.replace(`<#${forkedThread.id}>`, '<#FORK_THREAD>');
|
|
661
|
+
expect(sourceThreadText.split('\n').slice(-3).join('\n')).toMatchInlineSnapshot(`
|
|
662
|
+
"Reply with exactly: btw-model-check. btw
|
|
663
|
+
--- from: assistant (TestBot)
|
|
664
|
+
Session forked! Continue in <#FORK_THREAD>"
|
|
665
|
+
`);
|
|
658
666
|
const forkedThreadText = (await discord.thread(forkedThread.id).text())
|
|
659
667
|
.replace(`<#${sourceThread.id}>`, '<#SOURCE_THREAD>');
|
|
660
668
|
expect(forkedThreadText).toMatchInlineSnapshot(`
|
|
661
669
|
"--- from: assistant (TestBot)
|
|
662
670
|
Reusing context from <#SOURCE_THREAD> to answer prompt...
|
|
663
671
|
Reply with exactly: btw-model-check
|
|
664
|
-
|
|
672
|
+
ok
|
|
665
673
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2*"
|
|
666
674
|
`);
|
|
667
675
|
expect(forkedSessionModel).toMatchInlineSnapshot(`
|
|
@@ -726,12 +734,12 @@ describe('agent model resolution', () => {
|
|
|
726
734
|
Reply with exactly: first-thread-msg
|
|
727
735
|
--- from: assistant (TestBot)
|
|
728
736
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
729
|
-
|
|
737
|
+
ok
|
|
730
738
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
731
739
|
--- from: user (agent-model-tester)
|
|
732
740
|
Reply with exactly: second-thread-msg
|
|
733
741
|
--- from: assistant (TestBot)
|
|
734
|
-
|
|
742
|
+
ok
|
|
735
743
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
736
744
|
`);
|
|
737
745
|
const secondMessages = await discord.thread(thread.id).getMessages();
|
|
@@ -780,7 +788,7 @@ describe('agent model resolution', () => {
|
|
|
780
788
|
"--- from: assistant (TestBot)
|
|
781
789
|
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-msg
|
|
782
790
|
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
783
|
-
|
|
791
|
+
ok
|
|
784
792
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
785
793
|
`);
|
|
786
794
|
}, 20_000);
|
|
@@ -816,7 +824,7 @@ describe('agent model resolution', () => {
|
|
|
816
824
|
"--- from: assistant (TestBot)
|
|
817
825
|
» **agent-model-tester** (plan): Reply with exactly: inline-plan-agent-variant-msg
|
|
818
826
|
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
819
|
-
|
|
827
|
+
ok
|
|
820
828
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
821
829
|
`);
|
|
822
830
|
expect(sessionId ? await getSessionModel(sessionId) : undefined).toMatchInlineSnapshot(`
|
|
@@ -909,10 +917,10 @@ describe('agent model resolution', () => {
|
|
|
909
917
|
Reply with exactly: inline-existing-first-msg
|
|
910
918
|
--- from: assistant (TestBot)
|
|
911
919
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
912
|
-
|
|
920
|
+
ok
|
|
913
921
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
914
922
|
» **agent-model-tester** (plan): Reply with exactly: inline-existing-plan-msg
|
|
915
|
-
|
|
923
|
+
ok
|
|
916
924
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
917
925
|
`);
|
|
918
926
|
}, 20_000);
|
|
@@ -963,7 +971,7 @@ describe('agent model resolution', () => {
|
|
|
963
971
|
Reply with exactly: switch-in-thread-msg
|
|
964
972
|
--- from: assistant (TestBot)
|
|
965
973
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
966
|
-
|
|
974
|
+
ok
|
|
967
975
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
968
976
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
969
977
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
@@ -971,7 +979,7 @@ describe('agent model resolution', () => {
|
|
|
971
979
|
--- from: user (agent-model-tester)
|
|
972
980
|
Reply with exactly: after-switch-msg
|
|
973
981
|
--- from: assistant (TestBot)
|
|
974
|
-
|
|
982
|
+
ok
|
|
975
983
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
976
984
|
`);
|
|
977
985
|
const secondFooter = [...(await discord.thread(thread.id).getMessages())]
|
|
@@ -1022,7 +1030,7 @@ describe('agent model resolution', () => {
|
|
|
1022
1030
|
Reply with exactly: race-switch-first-msg
|
|
1023
1031
|
--- from: assistant (TestBot)
|
|
1024
1032
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1025
|
-
|
|
1033
|
+
ok
|
|
1026
1034
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
1027
1035
|
--- from: user (agent-model-tester)
|
|
1028
1036
|
Reply with exactly: race-switch-follow-up
|
|
@@ -1030,7 +1038,7 @@ describe('agent model resolution', () => {
|
|
|
1030
1038
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
1031
1039
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
1032
1040
|
The agent will change on the next message.
|
|
1033
|
-
|
|
1041
|
+
ok
|
|
1034
1042
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
1035
1043
|
`);
|
|
1036
1044
|
const secondFooter = [...(await discord.thread(thread.id).getMessages())]
|
|
@@ -1072,7 +1080,7 @@ describe('agent model resolution', () => {
|
|
|
1072
1080
|
Reply with exactly: race-channel-follow-up
|
|
1073
1081
|
--- from: assistant (TestBot)
|
|
1074
1082
|
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1075
|
-
|
|
1083
|
+
ok
|
|
1076
1084
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***"
|
|
1077
1085
|
`);
|
|
1078
1086
|
const footer = [...(await discord.thread(thread.id).getMessages())]
|
|
@@ -1120,7 +1128,7 @@ describe('agent model resolution', () => {
|
|
|
1120
1128
|
Reply with exactly: refresh-agent-model-msg
|
|
1121
1129
|
--- from: assistant (TestBot)
|
|
1122
1130
|
-# *using deterministic-provider/plan-model-v2 ⋅ plan*
|
|
1123
|
-
|
|
1131
|
+
ok
|
|
1124
1132
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ plan-model-v2 ⋅ **plan***
|
|
1125
1133
|
Using **plan** agent for this session
|
|
1126
1134
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
@@ -1162,7 +1170,7 @@ describe('agent model resolution', () => {
|
|
|
1162
1170
|
Reply with exactly: channel-vs-agent-msg
|
|
1163
1171
|
--- from: assistant (TestBot)
|
|
1164
1172
|
-# *using deterministic-provider/agent-model-v2 ⋅ test-agent*
|
|
1165
|
-
|
|
1173
|
+
ok
|
|
1166
1174
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
1167
1175
|
Switched to **plan** agent for this session (was **test-agent**)
|
|
1168
1176
|
Model: *deterministic-provider/plan-model-v2* (agent "plan")
|
|
@@ -1204,7 +1212,7 @@ describe('agent model resolution', () => {
|
|
|
1204
1212
|
Reply with exactly: plain-agent-override-msg
|
|
1205
1213
|
--- from: assistant (TestBot)
|
|
1206
1214
|
-# *using deterministic-provider/channel-model-v2*
|
|
1207
|
-
|
|
1215
|
+
ok
|
|
1208
1216
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*
|
|
1209
1217
|
Switched to **plain** agent for this session
|
|
1210
1218
|
Model: *deterministic-provider/channel-model-v2* (channel override)
|
package/dist/analytics.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Anonymous product analytics via Strada (
|
|
1
|
+
// Anonymous product analytics via Strada (@strada.sh/sdk, no dependencies).
|
|
2
2
|
// Tracks install-level usage only: no Discord IDs, paths, prompts, or secrets.
|
|
3
3
|
// A random install id is stored in {dataDir}/install-id for DAU-style queries.
|
|
4
4
|
//
|
|
@@ -158,6 +158,8 @@ export function initAnalytics() {
|
|
|
158
158
|
environment,
|
|
159
159
|
version: getKimakiVersion(),
|
|
160
160
|
userId: installId,
|
|
161
|
+
// Kimaki owns crash handling in discord-bot.ts; the SDK must not exit the process.
|
|
162
|
+
captureUncaughtErrors: false,
|
|
161
163
|
});
|
|
162
164
|
initialized = true;
|
|
163
165
|
}
|
|
@@ -6,10 +6,19 @@
|
|
|
6
6
|
// - punctuation + btw: ". btw", "! btw", ". btw.", "!btw."
|
|
7
7
|
// - btw as its own final line: "text\nbtw"
|
|
8
8
|
// Non-matches: "btw fix this" (start only), "hello btw" (no punctuation)
|
|
9
|
+
import { extractQueueSuffix } from './message-formatting.js';
|
|
9
10
|
const BTW_SUFFIX_RE = /(?:[.!?,;:])\s*btw\.?\s*$|\n\s*btw\.?\s*$/i;
|
|
11
|
+
const BTW_QUEUE_SUFFIX_RE = /(?:[.!?,;:])\s*btw\s+queue\.?\s*$|\n\s*btw\s+queue\.?\s*$/i;
|
|
10
12
|
export function extractBtwSuffix(content) {
|
|
11
13
|
if (!BTW_SUFFIX_RE.test(content)) {
|
|
12
14
|
return { prompt: content, forceBtw: false };
|
|
13
15
|
}
|
|
14
16
|
return { prompt: content.replace(BTW_SUFFIX_RE, '').trimEnd(), forceBtw: true };
|
|
15
17
|
}
|
|
18
|
+
export function extractBtwQueueSuffix(content) {
|
|
19
|
+
const queued = BTW_QUEUE_SUFFIX_RE.test(content)
|
|
20
|
+
? { prompt: content.replace(/\s+queue\.?\s*$/i, ''), forceQueue: true }
|
|
21
|
+
: extractQueueSuffix(content);
|
|
22
|
+
const btw = extractBtwSuffix(queued.prompt);
|
|
23
|
+
return { prompt: btw.prompt, forceQueue: queued.forceQueue, forceBtw: btw.forceBtw };
|
|
24
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import { extractBtwSuffix } from './btw-prefix-detection.js';
|
|
2
|
+
import { extractBtwSuffix, extractBtwQueueSuffix } from './btw-prefix-detection.js';
|
|
3
3
|
describe('extractBtwSuffix', () => {
|
|
4
4
|
test('matches after period', () => {
|
|
5
5
|
expect(extractBtwSuffix('fix the bug. btw')).toMatchInlineSnapshot(`
|
|
@@ -91,3 +91,40 @@ describe('extractBtwSuffix', () => {
|
|
|
91
91
|
`);
|
|
92
92
|
});
|
|
93
93
|
});
|
|
94
|
+
test('btw queue suffix keeps both actions in order', () => {
|
|
95
|
+
expect([
|
|
96
|
+
'Explain this. btw queue',
|
|
97
|
+
'Explain this. btw. queue',
|
|
98
|
+
'Explain this\nbtw queue',
|
|
99
|
+
'Explain this. btw',
|
|
100
|
+
'Explain this. queue',
|
|
101
|
+
].map(extractBtwQueueSuffix)).toMatchInlineSnapshot(`
|
|
102
|
+
[
|
|
103
|
+
{
|
|
104
|
+
"forceBtw": true,
|
|
105
|
+
"forceQueue": true,
|
|
106
|
+
"prompt": "Explain this",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"forceBtw": true,
|
|
110
|
+
"forceQueue": true,
|
|
111
|
+
"prompt": "Explain this",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"forceBtw": true,
|
|
115
|
+
"forceQueue": true,
|
|
116
|
+
"prompt": "Explain this",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"forceBtw": true,
|
|
120
|
+
"forceQueue": false,
|
|
121
|
+
"prompt": "Explain this",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"forceBtw": false,
|
|
125
|
+
"forceQueue": true,
|
|
126
|
+
"prompt": "Explain this",
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
`);
|
|
130
|
+
});
|
|
@@ -37,7 +37,7 @@ cli
|
|
|
37
37
|
.alias('start-session') // backwards compatibility
|
|
38
38
|
.option('-c, --channel <channelId>', 'Discord channel ID')
|
|
39
39
|
.option('-d, --project <path>', 'Project directory (alternative to --channel)')
|
|
40
|
-
.option('-p, --prompt <prompt>', 'Message content')
|
|
40
|
+
.option('-p, --prompt <prompt>', 'Message content. With --thread/--session, end with ". queue" to wait for the current run instead of interrupting it, ". btw" to fork a side session now, or ". btw queue" to fork after the current run')
|
|
41
41
|
.option('-n, --name [name]', 'Thread name (optional, defaults to prompt preview)')
|
|
42
42
|
.option('-a, --app-id [appId]', 'Bot application ID (required if no local database)')
|
|
43
43
|
.option('--notify-only', 'Create notification thread without starting AI session')
|
|
@@ -55,7 +55,7 @@ cli
|
|
|
55
55
|
.option('--send-at <schedule>', 'Schedule send for future (UTC ISO date/time ending in Z, or cron expression)')
|
|
56
56
|
.option('--pre-run <command>', 'Run a shell command in the project before starting a scheduled task')
|
|
57
57
|
.option('--allow-concurrency', 'Allow concurrent sessions from the same scheduled task')
|
|
58
|
-
.option('--thread <threadId>', 'Post prompt to an existing thread')
|
|
58
|
+
.option('--thread <threadId>', 'Post prompt to an existing thread. Interrupts a busy session unless the prompt ends with ". queue"')
|
|
59
59
|
.option('--session <sessionId>', 'Post prompt to thread mapped to an existing session')
|
|
60
60
|
.option('--parent-session <sessionId>', 'Parent OpenCode session ID for newly created child sessions')
|
|
61
61
|
.option('--wait', 'Wait for session to complete, then print session text to stdout')
|
package/dist/cli-runner.js
CHANGED
|
@@ -9,6 +9,7 @@ import * as dbSchema from './schema.js';
|
|
|
9
9
|
import { selectResolvedCommand } from './opencode-command.js';
|
|
10
10
|
import { Events, ChannelType, Routes, AttachmentBuilder, } from 'discord.js';
|
|
11
11
|
import { discordApiUrl, getDiscordRestApiUrl, getGatewayProxyRestBaseUrl, getInternetReachableBaseUrl } from './discord-urls.js';
|
|
12
|
+
import { extractBtwQueueSuffix } from './btw-prefix-detection.js';
|
|
12
13
|
import crypto from 'node:crypto';
|
|
13
14
|
import path from 'node:path';
|
|
14
15
|
import fs from 'node:fs';
|
|
@@ -160,6 +161,18 @@ function wrapPromptAttachmentText(prompt) {
|
|
|
160
161
|
})
|
|
161
162
|
.join('\n');
|
|
162
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Visible message content + prompt.md text for prompts over Discord's limit.
|
|
166
|
+
* The bot reads queue/btw suffixes only from message content, so the suffix
|
|
167
|
+
* moves from the file to the visible message and the send still queues or forks.
|
|
168
|
+
*/
|
|
169
|
+
export function buildLongPromptMessage(prompt) {
|
|
170
|
+
const { prompt: fileText, forceQueue, forceBtw } = extractBtwQueueSuffix(prompt);
|
|
171
|
+
const preview = fileText.slice(0, 100).replace(/\n/g, ' ');
|
|
172
|
+
const summary = `Prompt attached as file (${fileText.length} chars)\n\n> ${preview}…`;
|
|
173
|
+
const suffix = [forceBtw && 'btw', forceQueue && 'queue'].filter(Boolean).join(' ');
|
|
174
|
+
return { content: suffix ? `${summary}\n\n${suffix}` : summary, fileText };
|
|
175
|
+
}
|
|
163
176
|
function promptAttachmentBlob(text) {
|
|
164
177
|
return new Blob([new Uint8Array(Buffer.from(text, 'utf8'))], {
|
|
165
178
|
type: 'text/markdown',
|
|
@@ -182,18 +195,18 @@ export async function sendDiscordMessageWithOptionalAttachment({ channelId, prom
|
|
|
182
195
|
// When prompt exceeds Discord's limit, attach it as prompt.md alongside
|
|
183
196
|
// user files so nothing is silently lost. Build prompt.md from memory so
|
|
184
197
|
// parallel kimaki send processes never share or unlink a temp path.
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
198
|
+
const longPrompt = prompt.length > discordMaxLength
|
|
199
|
+
? buildLongPromptMessage(prompt)
|
|
200
|
+
: undefined;
|
|
201
|
+
const content = longPrompt ? longPrompt.content : prompt;
|
|
189
202
|
const allFiles = files.map((file) => ({
|
|
190
203
|
data: new Uint8Array(fs.readFileSync(file)),
|
|
191
204
|
filename: path.basename(file),
|
|
192
205
|
mimeType: mime.getType(file) || 'application/octet-stream',
|
|
193
206
|
}));
|
|
194
|
-
if (
|
|
207
|
+
if (longPrompt) {
|
|
195
208
|
allFiles.push({
|
|
196
|
-
data: new Uint8Array(Buffer.from(
|
|
209
|
+
data: new Uint8Array(Buffer.from(longPrompt.fileText, 'utf8')),
|
|
197
210
|
filename: 'prompt.md',
|
|
198
211
|
mimeType: 'text/markdown',
|
|
199
212
|
});
|
|
@@ -259,17 +272,16 @@ export async function sendDiscordMessageWithOptionalAttachment({ channelId, prom
|
|
|
259
272
|
}
|
|
260
273
|
return firstMessage;
|
|
261
274
|
}
|
|
262
|
-
const
|
|
263
|
-
const summaryContent = `Prompt attached as file (${prompt.length} chars)\n\n> ${preview}...`;
|
|
275
|
+
const longPrompt = buildLongPromptMessage(prompt);
|
|
264
276
|
// In-memory Blob only — no temp file. Parallel send must never share a path.
|
|
265
277
|
const formData = new FormData();
|
|
266
278
|
formData.append('payload_json', JSON.stringify({
|
|
267
|
-
content:
|
|
279
|
+
content: longPrompt.content,
|
|
268
280
|
attachments: [{ id: 0, filename: 'prompt.md' }],
|
|
269
281
|
embeds,
|
|
270
282
|
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
271
283
|
}));
|
|
272
|
-
formData.append('files[0]', promptAttachmentBlob(wrapPromptAttachmentText(
|
|
284
|
+
formData.append('files[0]', promptAttachmentBlob(wrapPromptAttachmentText(longPrompt.fileText)), 'prompt.md');
|
|
273
285
|
const starterMessageResponse = await fetch(discordApiUrl(`/channels/${channelId}/messages`), {
|
|
274
286
|
method: 'POST',
|
|
275
287
|
headers: {
|
package/dist/cli-runner.test.js
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
2
|
import { REST } from 'discord.js';
|
|
3
|
-
import { getOpenUrlCommand, isTransientNetworkError, resolveDiscordUserOption, } from './cli-runner.js';
|
|
3
|
+
import { buildLongPromptMessage, getOpenUrlCommand, isTransientNetworkError, resolveDiscordUserOption, } from './cli-runner.js';
|
|
4
|
+
import { extractBtwQueueSuffix } from './btw-prefix-detection.js';
|
|
5
|
+
// Regression: a long `. btw` prompt lost its suffix and interrupted the source session.
|
|
6
|
+
test('long prompts keep queue and btw suffixes in the visible message', () => {
|
|
7
|
+
const body = 'x'.repeat(2100);
|
|
8
|
+
expect(['queue', 'btw', 'btw queue', ''].map((suffix) => {
|
|
9
|
+
const { content, fileText } = buildLongPromptMessage(suffix ? `${body}. ${suffix}` : body);
|
|
10
|
+
const parsed = extractBtwQueueSuffix(content);
|
|
11
|
+
return { fileText: fileText === body, forceQueue: parsed.forceQueue, forceBtw: parsed.forceBtw };
|
|
12
|
+
})).toMatchInlineSnapshot(`
|
|
13
|
+
[
|
|
14
|
+
{
|
|
15
|
+
"fileText": true,
|
|
16
|
+
"forceBtw": false,
|
|
17
|
+
"forceQueue": true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"fileText": true,
|
|
21
|
+
"forceBtw": true,
|
|
22
|
+
"forceQueue": false,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"fileText": true,
|
|
26
|
+
"forceBtw": true,
|
|
27
|
+
"forceQueue": true,
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"fileText": true,
|
|
31
|
+
"forceBtw": false,
|
|
32
|
+
"forceQueue": false,
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
`);
|
|
36
|
+
});
|
|
4
37
|
test('raw Discord user ID does not invent a username', async () => {
|
|
5
38
|
const user = await resolveDiscordUserOption({
|
|
6
39
|
user: '535922349652836367',
|
|
@@ -320,7 +320,7 @@ describe('kimaki send --channel thread creation', () => {
|
|
|
320
320
|
thread empty content seed
|
|
321
321
|
--- from: assistant (TestBot)
|
|
322
322
|
-# *using deterministic-provider/deterministic-v2*
|
|
323
|
-
|
|
323
|
+
caught-by-model
|
|
324
324
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
325
325
|
--- from: user (cli-send-tester)
|
|
326
326
|
--- from: assistant (TestBot)
|
package/dist/commands/btw.js
CHANGED
|
@@ -8,9 +8,19 @@ import { resolveWorkingDirectory, resolveTextChannel, sendThreadMessage, } from
|
|
|
8
8
|
import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
|
|
9
9
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
10
10
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
11
|
-
import {
|
|
11
|
+
import { copySessionPreferences } from './model.js';
|
|
12
|
+
import { copySessionSystemPrompt } from '../system-message.js';
|
|
13
|
+
import { OpenCodeSdkError } from '../errors.js';
|
|
14
|
+
import { extractQueueSuffix } from '../message-formatting.js';
|
|
12
15
|
const logger = createLogger(LogPrefix.FORK);
|
|
16
|
+
// Resolves to [value, elapsed ms] so each btw setup step can be logged.
|
|
17
|
+
async function timed(promise) {
|
|
18
|
+
const start = Date.now();
|
|
19
|
+
const value = await promise;
|
|
20
|
+
return [value, Date.now() - start];
|
|
21
|
+
}
|
|
13
22
|
export async function forkSessionToBtwThread({ sourceThread, projectDirectory, sdkDirectory, prompt, modelPrompt = prompt, userId, username, appId, agent, images, }) {
|
|
23
|
+
const startedAt = Date.now();
|
|
14
24
|
// Parallelize: session lookup + opencode init + parent channel resolve are independent
|
|
15
25
|
const [sessionId, getClientResult, textChannel] = await Promise.all([
|
|
16
26
|
getThreadSession(sourceThread.id),
|
|
@@ -28,51 +38,106 @@ export async function forkSessionToBtwThread({ sourceThread, projectDirectory, s
|
|
|
28
38
|
if (!textChannel) {
|
|
29
39
|
return new Error('Could not resolve parent text channel');
|
|
30
40
|
}
|
|
31
|
-
// Fork
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
// Fork and thread creation are independent round trips, so run them together.
|
|
42
|
+
// If either side fails, remove whichever side succeeded.
|
|
43
|
+
const initMs = Date.now() - startedAt;
|
|
44
|
+
const [forkSettled, threadSettled] = await Promise.allSettled([
|
|
45
|
+
timed(getClientResult().session.fork({ sessionID: sessionId, directory: sdkDirectory })),
|
|
46
|
+
timed(textChannel.threads.create({
|
|
47
|
+
name: `btw: ${prompt}`.slice(0, 100),
|
|
48
|
+
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
49
|
+
reason: `btw fork from session ${sessionId}`,
|
|
50
|
+
})),
|
|
51
|
+
]);
|
|
52
|
+
const forkedSession = forkSettled.status === 'fulfilled' ? forkSettled.value[0].data : undefined;
|
|
53
|
+
const createdThread = threadSettled.status === 'fulfilled' ? threadSettled.value[0] : undefined;
|
|
54
|
+
const cleanup = async () => {
|
|
55
|
+
await Promise.all([
|
|
56
|
+
createdThread?.delete('btw fork setup failed').catch((error) => {
|
|
57
|
+
logger.warn(`Could not delete orphan btw thread ${createdThread.id}:`, error);
|
|
58
|
+
}),
|
|
59
|
+
forkedSession && getClientResult()
|
|
60
|
+
.session.delete({ sessionID: forkedSession.id, directory: sdkDirectory })
|
|
61
|
+
.catch((error) => {
|
|
62
|
+
logger.warn(`Could not delete orphan btw session ${forkedSession.id}:`, error);
|
|
63
|
+
}),
|
|
64
|
+
]);
|
|
65
|
+
};
|
|
66
|
+
if (!forkedSession) {
|
|
67
|
+
await cleanup();
|
|
68
|
+
const cause = forkSettled.status === 'rejected' ? forkSettled.reason : forkSettled.value[0].error;
|
|
69
|
+
return new OpenCodeSdkError({ operation: 'session.fork', cause });
|
|
35
70
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
targetSessionId: forkedSession.id,
|
|
41
|
-
channelId,
|
|
42
|
-
appId,
|
|
43
|
-
getClient: getClientResult,
|
|
44
|
-
directory: sdkDirectory,
|
|
45
|
-
});
|
|
46
|
-
const thread = await textChannel.threads.create({
|
|
47
|
-
name: `btw: ${prompt}`.slice(0, 100),
|
|
48
|
-
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
49
|
-
reason: `btw fork from session ${sessionId}`,
|
|
50
|
-
});
|
|
51
|
-
// DB mapping must complete before user-visible actions so the thread is routable
|
|
52
|
-
await setThreadSession(thread.id, forkedSession.id);
|
|
53
|
-
const sourceWorkspace = await getThreadWorktreeOrWorkspace(sourceThread.id);
|
|
54
|
-
if (sourceWorkspace?.status === 'ready' && sourceWorkspace.workspace_directory) {
|
|
55
|
-
await createPendingWorkspace({
|
|
56
|
-
threadId: thread.id,
|
|
57
|
-
workspaceType: sourceWorkspace.workspace_type,
|
|
58
|
-
workspaceName: sourceWorkspace.workspace_name ?? '',
|
|
59
|
-
projectDirectory,
|
|
60
|
-
});
|
|
61
|
-
await setWorkspaceReady({
|
|
62
|
-
threadId: thread.id,
|
|
63
|
-
workspaceId: sourceWorkspace.workspace_id ?? undefined,
|
|
64
|
-
workspaceDirectory: sourceWorkspace.workspace_directory,
|
|
71
|
+
if (!createdThread) {
|
|
72
|
+
await cleanup();
|
|
73
|
+
return new Error('Failed to create the btw thread', {
|
|
74
|
+
cause: threadSettled.status === 'rejected' ? threadSettled.reason : undefined,
|
|
65
75
|
});
|
|
66
76
|
}
|
|
67
|
-
|
|
77
|
+
const thread = createdThread;
|
|
78
|
+
const forkMs = forkSettled.status === 'fulfilled' ? forkSettled.value[1] : -1;
|
|
79
|
+
const threadMs = threadSettled.status === 'fulfilled' ? threadSettled.value[1] : -1;
|
|
80
|
+
const channelId = sourceThread.parentId || sourceThread.id;
|
|
68
81
|
const sourceThreadLink = `<#${sourceThread.id}>`;
|
|
82
|
+
// The fork must run with the source agent, model and pinned system prompt,
|
|
83
|
+
// so its request prefix is byte-identical and hits the source prompt cache.
|
|
84
|
+
// `false` (source not pinned yet) is the only fallback; I/O errors fail setup.
|
|
85
|
+
const copyStartedAt = Date.now();
|
|
86
|
+
const [, copiedSystem, history] = await Promise.all([
|
|
87
|
+
copySessionPreferences({
|
|
88
|
+
sourceSessionId: sessionId,
|
|
89
|
+
targetSessionId: forkedSession.id,
|
|
90
|
+
channelId,
|
|
91
|
+
appId,
|
|
92
|
+
getClient: getClientResult,
|
|
93
|
+
directory: sdkDirectory,
|
|
94
|
+
}),
|
|
95
|
+
copySessionSystemPrompt({
|
|
96
|
+
sourceSessionId: sessionId,
|
|
97
|
+
targetSessionId: forkedSession.id,
|
|
98
|
+
}),
|
|
99
|
+
getClientResult().session.messages({
|
|
100
|
+
sessionID: forkedSession.id,
|
|
101
|
+
directory: sdkDirectory,
|
|
102
|
+
limit: 10,
|
|
103
|
+
}).catch((cause) => new OpenCodeSdkError({ operation: 'session.messages', cause })),
|
|
104
|
+
]);
|
|
105
|
+
if (copiedSystem instanceof Error) {
|
|
106
|
+
await cleanup();
|
|
107
|
+
return new Error(`Could not copy the source system prompt to the fork: ${copiedSystem.message}`, {
|
|
108
|
+
cause: copiedSystem,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const copyMs = Date.now() - copyStartedAt;
|
|
112
|
+
const routeStartedAt = Date.now();
|
|
69
113
|
await Promise.all([
|
|
114
|
+
// DB mapping must complete before dispatch so the thread is routable
|
|
115
|
+
(async () => {
|
|
116
|
+
await setThreadSession(thread.id, forkedSession.id);
|
|
117
|
+
const sourceWorkspace = await getThreadWorktreeOrWorkspace(sourceThread.id);
|
|
118
|
+
if (sourceWorkspace?.status !== 'ready' || !sourceWorkspace.workspace_directory) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
await createPendingWorkspace({
|
|
122
|
+
threadId: thread.id,
|
|
123
|
+
workspaceType: sourceWorkspace.workspace_type,
|
|
124
|
+
workspaceName: sourceWorkspace.workspace_name ?? '',
|
|
125
|
+
projectDirectory,
|
|
126
|
+
});
|
|
127
|
+
await setWorkspaceReady({
|
|
128
|
+
threadId: thread.id,
|
|
129
|
+
workspaceId: sourceWorkspace.workspace_id ?? undefined,
|
|
130
|
+
workspaceDirectory: sourceWorkspace.workspace_directory,
|
|
131
|
+
});
|
|
132
|
+
})(),
|
|
70
133
|
thread.members.add(userId).catch((error) => {
|
|
71
134
|
logger.warn('Could not add fork member:', error);
|
|
72
135
|
}),
|
|
73
136
|
sendThreadMessage(thread, `Reusing context from ${sourceThreadLink} to answer prompt...\n${prompt}`),
|
|
74
137
|
]);
|
|
75
|
-
|
|
138
|
+
const routeMs = Date.now() - routeStartedAt;
|
|
139
|
+
logger.log(`Created btw fork session ${forkedSession.id} in thread ${thread.id} from source thread ${sourceThread.id} (session ${sessionId}), system prompt ${copiedSystem ? 'reused' : 'regenerated'}`);
|
|
140
|
+
logger.log(`[BTW TIMING] ${forkedSession.id} init=${initMs}ms fork=${forkMs}ms threadCreate=${threadMs}ms copyPrefs=${copyMs}ms route=${routeMs}ms total=${Date.now() - startedAt}ms`);
|
|
76
141
|
// Parent context stays in the user prompt only. Do NOT pass parentSessionId
|
|
77
142
|
// into enqueueIncoming: that would inject a parent block into the system
|
|
78
143
|
// message and bust prompt cache shared with the parent session.
|
|
@@ -95,7 +160,18 @@ export async function forkSessionToBtwThread({ sourceThread, projectDirectory, s
|
|
|
95
160
|
appId,
|
|
96
161
|
sessionId: forkedSession.id,
|
|
97
162
|
});
|
|
98
|
-
|
|
163
|
+
if (history instanceof Error || history.error) {
|
|
164
|
+
logger.warn('Could not load copied messages for btw cache diagnostics:', history);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const last = history.data?.findLast(({ info }) => info.role === 'assistant' && typeof info.time.completed === 'number')?.info;
|
|
168
|
+
if (last?.role === 'assistant') {
|
|
169
|
+
runtime.seedForkPromptCacheBaseline(last);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
// Not awaited: the caller confirms in the source thread right away while the
|
|
173
|
+
// runtime resolves preferences and dispatches. Failures are reported in the fork.
|
|
174
|
+
void runtime.enqueueIncoming({
|
|
99
175
|
prompt: wrappedPrompt,
|
|
100
176
|
agent,
|
|
101
177
|
images,
|
|
@@ -103,7 +179,9 @@ export async function forkSessionToBtwThread({ sourceThread, projectDirectory, s
|
|
|
103
179
|
username,
|
|
104
180
|
appId,
|
|
105
181
|
mode: 'opencode',
|
|
106
|
-
}).
|
|
182
|
+
}).then(() => {
|
|
183
|
+
logger.log(`[BTW TIMING] ${forkedSession.id} promptAccepted=${Date.now() - startedAt}ms`);
|
|
184
|
+
}, async (error) => {
|
|
107
185
|
logger.error('Fork dispatch failed:', error);
|
|
108
186
|
await sendThreadMessage(thread, 'Could not send the request to OpenCode. Send your request again in this thread.');
|
|
109
187
|
});
|
|
@@ -131,7 +209,11 @@ export async function handleBtwCommand({ command, appId, }) {
|
|
|
131
209
|
return;
|
|
132
210
|
}
|
|
133
211
|
const threadChannel = channel;
|
|
134
|
-
const prompt = command.options.getString('prompt', true);
|
|
212
|
+
const { prompt, forceQueue } = extractQueueSuffix(command.options.getString('prompt', true));
|
|
213
|
+
if (!prompt.trim()) {
|
|
214
|
+
await command.reply({ content: 'Write a question to fork', flags: MessageFlags.Ephemeral });
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
135
217
|
const resolved = await resolveWorkingDirectory({
|
|
136
218
|
channel: threadChannel,
|
|
137
219
|
});
|
|
@@ -145,6 +227,32 @@ export async function handleBtwCommand({ command, appId, }) {
|
|
|
145
227
|
const { projectDirectory, workingDirectory } = resolved;
|
|
146
228
|
await command.deferReply({ flags: MessageFlags.Ephemeral });
|
|
147
229
|
try {
|
|
230
|
+
if (forceQueue) {
|
|
231
|
+
if (!await getThreadSession(threadChannel.id)) {
|
|
232
|
+
await command.editReply('No active session in this thread. Start a session, then retry /btw.');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const runtime = getOrCreateRuntime({
|
|
236
|
+
threadId: threadChannel.id,
|
|
237
|
+
thread: threadChannel,
|
|
238
|
+
projectDirectory,
|
|
239
|
+
sdkDirectory: workingDirectory,
|
|
240
|
+
channelId: threadChannel.parentId || threadChannel.id,
|
|
241
|
+
appId,
|
|
242
|
+
});
|
|
243
|
+
const queued = await runtime.enqueueIncoming({
|
|
244
|
+
prompt,
|
|
245
|
+
queuedAction: 'btw',
|
|
246
|
+
userId: command.user.id,
|
|
247
|
+
username: command.user.displayName,
|
|
248
|
+
appId,
|
|
249
|
+
mode: 'local-queue',
|
|
250
|
+
});
|
|
251
|
+
await command.editReply(queued.queued
|
|
252
|
+
? `Btw fork queued at position ${queued.position}. It will start after the earlier prompts finish.`
|
|
253
|
+
: 'Btw fork started. The new thread will appear shortly.');
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
148
256
|
const result = await forkSessionToBtwThread({
|
|
149
257
|
sourceThread: threadChannel,
|
|
150
258
|
projectDirectory,
|