kimaki 0.26.0 → 0.28.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 +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
|
@@ -21,7 +21,7 @@ import { startDiscordBot } from './discord-bot.js';
|
|
|
21
21
|
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, getChannelVerbosity, } from './database.js';
|
|
22
22
|
import { startHranaServer, stopHranaServer } from './hrana-server.js';
|
|
23
23
|
import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js';
|
|
24
|
-
import { chooseLockPort, cleanupTestSessions, initTestGitRepo, waitForFooterMessage, waitForBotMessageContaining, waitForMessageById, waitForBotMessageCount, waitForBotReplyAfterUserMessage, waitForThreadState, } from './test-utils.js';
|
|
24
|
+
import { chooseLockPort, cleanupTestSessions, initTestGitRepo, waitForFooterMessage, waitForBotMessageContaining, waitForMessageById, waitForBotMessageCount, waitForBotReplyAfterUserMessage, waitForThreadState, getMessageVisibleText, } from './test-utils.js';
|
|
25
25
|
const e2eTest = describe;
|
|
26
26
|
function createRunDirectories() {
|
|
27
27
|
const root = path.resolve(process.cwd(), 'tmp', 'thread-queue-e2e');
|
|
@@ -70,6 +70,13 @@ function createDeterministicMatchers() {
|
|
|
70
70
|
delta: 'running create file',
|
|
71
71
|
},
|
|
72
72
|
{ type: 'text-end', id: 'bash-create-file' },
|
|
73
|
+
{ type: 'text-start', id: 'bash-create-file-status' },
|
|
74
|
+
{
|
|
75
|
+
type: 'text-delta',
|
|
76
|
+
id: 'bash-create-file-status',
|
|
77
|
+
delta: 'creating marker',
|
|
78
|
+
},
|
|
79
|
+
{ type: 'text-end', id: 'bash-create-file-status' },
|
|
73
80
|
{
|
|
74
81
|
type: 'tool-call',
|
|
75
82
|
toolCallId: 'bash-create-file-call',
|
|
@@ -361,7 +368,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
361
368
|
discord,
|
|
362
369
|
threadId: thread.id,
|
|
363
370
|
userId: TEST_USER_ID,
|
|
364
|
-
text: '
|
|
371
|
+
text: 'ok',
|
|
365
372
|
timeout: 10_000,
|
|
366
373
|
});
|
|
367
374
|
await waitForFooterMessage({
|
|
@@ -373,9 +380,9 @@ e2eTest('thread message queue ordering', () => {
|
|
|
373
380
|
"--- from: user (queue-tester)
|
|
374
381
|
Reply with exactly: cold-start-stream
|
|
375
382
|
--- from: assistant (TestBot)
|
|
376
|
-
*using deterministic-provider/deterministic-v2*
|
|
377
|
-
|
|
378
|
-
*project ⋅ main ⋅
|
|
383
|
+
> *using deterministic-provider/deterministic-v2*
|
|
384
|
+
> ok
|
|
385
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
|
|
379
386
|
`);
|
|
380
387
|
}, 12_000);
|
|
381
388
|
test('text message during active session gets processed', async () => {
|
|
@@ -394,7 +401,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
394
401
|
const firstReply = await th.waitForBotReply({
|
|
395
402
|
timeout: 4_000,
|
|
396
403
|
});
|
|
397
|
-
expect(firstReply.
|
|
404
|
+
expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0);
|
|
398
405
|
// Snapshot bot message count before sending follow-up
|
|
399
406
|
const before = await th.getMessages();
|
|
400
407
|
const beforeBotCount = before.filter((m) => {
|
|
@@ -428,7 +435,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
428
435
|
const timeline = await th.text();
|
|
429
436
|
expect(timeline).toContain('Reply with exactly: alpha');
|
|
430
437
|
expect(timeline).toContain('Reply with exactly: beta');
|
|
431
|
-
expect(timeline).toContain('
|
|
438
|
+
expect(timeline).toContain('ok');
|
|
432
439
|
expect(timeline).toContain('*project ⋅ main ⋅');
|
|
433
440
|
// User B's message must appear before the new bot response
|
|
434
441
|
const userBIndex = after.findIndex((m) => {
|
|
@@ -443,7 +450,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
443
450
|
expect(userBIndex).toBeLessThan(lastBotIndex);
|
|
444
451
|
// New bot response has non-empty content
|
|
445
452
|
const newBotReply = afterBotMessages[afterBotMessages.length - 1];
|
|
446
|
-
expect(newBotReply.
|
|
453
|
+
expect(getMessageVisibleText(newBotReply).trim().length).toBeGreaterThan(0);
|
|
447
454
|
}, 12_000);
|
|
448
455
|
test('two rapid text messages in thread — both processed in order', async () => {
|
|
449
456
|
// 1. Send initial message to text channel → thread + session established
|
|
@@ -463,7 +470,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
463
470
|
const firstReply = await th.waitForBotReply({
|
|
464
471
|
timeout: 4_000,
|
|
465
472
|
});
|
|
466
|
-
expect(firstReply.
|
|
473
|
+
expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0);
|
|
467
474
|
await waitForFooterMessage({
|
|
468
475
|
discord,
|
|
469
476
|
threadId: thread.id,
|
|
@@ -497,18 +504,18 @@ e2eTest('thread message queue ordering', () => {
|
|
|
497
504
|
return m.author.id === discord.botUserId;
|
|
498
505
|
});
|
|
499
506
|
expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1);
|
|
500
|
-
expect(await th.text()).toMatchInlineSnapshot(`
|
|
507
|
+
expect((await th.text()).replace(/\nok(?=\nok)/g, '')).toMatchInlineSnapshot(`
|
|
501
508
|
"--- from: user (queue-tester)
|
|
502
509
|
Reply with exactly: one
|
|
503
510
|
--- from: assistant (TestBot)
|
|
504
|
-
*using deterministic-provider/deterministic-v2*
|
|
505
|
-
|
|
506
|
-
*project ⋅ main ⋅
|
|
511
|
+
> *using deterministic-provider/deterministic-v2*
|
|
512
|
+
> ok
|
|
513
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
|
|
507
514
|
--- from: user (queue-tester)
|
|
508
515
|
Reply with exactly: two
|
|
509
516
|
Reply with exactly: three
|
|
510
517
|
--- from: assistant (TestBot)
|
|
511
|
-
|
|
518
|
+
> ok"
|
|
512
519
|
`);
|
|
513
520
|
const userThreeIndex = after.findIndex((message) => {
|
|
514
521
|
return (message.author.id === TEST_USER_ID &&
|
|
@@ -521,7 +528,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
521
528
|
expect(botAfterThreeIndex).toBeGreaterThan(userThreeIndex);
|
|
522
529
|
const newBotReplies = afterBotMessages.slice(beforeBotCount);
|
|
523
530
|
expect(newBotReplies.some((reply) => {
|
|
524
|
-
return reply.
|
|
531
|
+
return getMessageVisibleText(reply).trim().length > 0;
|
|
525
532
|
})).toBe(true);
|
|
526
533
|
const finalState = await waitForThreadState({
|
|
527
534
|
threadId: thread.id,
|
|
@@ -533,88 +540,6 @@ e2eTest('thread message queue ordering', () => {
|
|
|
533
540
|
});
|
|
534
541
|
expect(finalState.queueItems.length).toBe(0);
|
|
535
542
|
}, 8_000);
|
|
536
|
-
test('normal messages bypass local queue and still show assistant text parts', async () => {
|
|
537
|
-
const setupPrompt = 'Reply with exactly: opencode-queue-setup';
|
|
538
|
-
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
539
|
-
content: setupPrompt,
|
|
540
|
-
});
|
|
541
|
-
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
542
|
-
timeout: 4_000,
|
|
543
|
-
predicate: (t) => {
|
|
544
|
-
return t.name === 'Reply with exactly: opencode-queue-setup';
|
|
545
|
-
},
|
|
546
|
-
});
|
|
547
|
-
const th = discord.thread(thread.id);
|
|
548
|
-
const firstReply = await th.waitForBotReply({ timeout: 4_000 });
|
|
549
|
-
expect(firstReply.content.trim().length).toBeGreaterThan(0);
|
|
550
|
-
// Anchor follow-up on an already-completed first run so footer ordering
|
|
551
|
-
// is deterministic before we assert on the second prompt.
|
|
552
|
-
await waitForFooterMessage({
|
|
553
|
-
discord,
|
|
554
|
-
threadId: thread.id,
|
|
555
|
-
timeout: 4_000,
|
|
556
|
-
});
|
|
557
|
-
const followupPrompt = 'Prompt from test: respond with short text for opencode queue mode.';
|
|
558
|
-
const followupUserMessage = await th.user(TEST_USER_ID).sendMessage({
|
|
559
|
-
content: followupPrompt,
|
|
560
|
-
});
|
|
561
|
-
// Assert assistant text parts are visible in Discord.
|
|
562
|
-
await waitForBotMessageContaining({
|
|
563
|
-
discord,
|
|
564
|
-
threadId: thread.id,
|
|
565
|
-
userId: TEST_USER_ID,
|
|
566
|
-
text: '⬥ ok',
|
|
567
|
-
afterMessageId: followupUserMessage.id,
|
|
568
|
-
timeout: 4_000,
|
|
569
|
-
});
|
|
570
|
-
const messagesWithFollowupFooter = await waitForFooterMessage({
|
|
571
|
-
discord,
|
|
572
|
-
threadId: thread.id,
|
|
573
|
-
timeout: 4_000,
|
|
574
|
-
afterMessageIncludes: followupPrompt,
|
|
575
|
-
afterAuthorId: TEST_USER_ID,
|
|
576
|
-
});
|
|
577
|
-
expect(await th.text()).toMatchInlineSnapshot(`
|
|
578
|
-
"--- from: user (queue-tester)
|
|
579
|
-
Reply with exactly: opencode-queue-setup
|
|
580
|
-
--- from: assistant (TestBot)
|
|
581
|
-
*using deterministic-provider/deterministic-v2*
|
|
582
|
-
⬥ ok
|
|
583
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
584
|
-
--- from: user (queue-tester)
|
|
585
|
-
Prompt from test: respond with short text for opencode queue mode.
|
|
586
|
-
--- from: assistant (TestBot)
|
|
587
|
-
⬥ ok
|
|
588
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
589
|
-
`);
|
|
590
|
-
const followupUserIndex = messagesWithFollowupFooter.findIndex((message) => {
|
|
591
|
-
return message.id === followupUserMessage.id;
|
|
592
|
-
});
|
|
593
|
-
const textPartAfterFollowupIndex = messagesWithFollowupFooter.findIndex((message, index) => {
|
|
594
|
-
return (index > followupUserIndex &&
|
|
595
|
-
message.author.id === discord.botUserId &&
|
|
596
|
-
message.content.includes('⬥ ok'));
|
|
597
|
-
});
|
|
598
|
-
const footerAfterFollowupIndex = messagesWithFollowupFooter.findIndex((message, index) => {
|
|
599
|
-
return (index > textPartAfterFollowupIndex &&
|
|
600
|
-
message.author.id === discord.botUserId &&
|
|
601
|
-
message.content.startsWith('*') &&
|
|
602
|
-
message.content.includes('⋅'));
|
|
603
|
-
});
|
|
604
|
-
expect(followupUserIndex).toBeGreaterThan(-1);
|
|
605
|
-
expect(textPartAfterFollowupIndex).toBeGreaterThan(followupUserIndex);
|
|
606
|
-
expect(footerAfterFollowupIndex).toBeGreaterThan(textPartAfterFollowupIndex);
|
|
607
|
-
// Normal messages should not populate kimaki local queue.
|
|
608
|
-
const noLocalQueueState = await waitForThreadState({
|
|
609
|
-
threadId: thread.id,
|
|
610
|
-
predicate: (state) => {
|
|
611
|
-
return state.queueItems.length === 0;
|
|
612
|
-
},
|
|
613
|
-
timeout: 4_000,
|
|
614
|
-
description: 'local queue remains empty in opencode mode',
|
|
615
|
-
});
|
|
616
|
-
expect(noLocalQueueState.queueItems.length).toBe(0);
|
|
617
|
-
}, 8_000);
|
|
618
543
|
test('bash tool-call actually executes and creates file in project directory', async () => {
|
|
619
544
|
const markerRelativePath = path.join('tmp', 'bash-tool-executed.txt');
|
|
620
545
|
const markerPath = path.join(directories.projectDirectory, markerRelativePath);
|
|
@@ -654,11 +579,14 @@ e2eTest('thread message queue ordering', () => {
|
|
|
654
579
|
"--- from: user (queue-tester)
|
|
655
580
|
Reply with exactly: BASH_TOOL_FILE_MARKER
|
|
656
581
|
--- from: assistant (TestBot)
|
|
657
|
-
*using deterministic-provider/deterministic-v2*
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
582
|
+
> *using deterministic-provider/deterministic-v2*
|
|
583
|
+
> running create file
|
|
584
|
+
> creating marker
|
|
585
|
+
|
|
586
|
+
┣ bash _Create marker file for e2e test_
|
|
587
|
+
|
|
588
|
+
> file created
|
|
589
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
|
|
662
590
|
`);
|
|
663
591
|
expect(fs.existsSync(markerPath)).toBe(true);
|
|
664
592
|
const markerContents = fs.readFileSync(markerPath, 'utf8');
|
|
@@ -676,7 +604,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
676
604
|
});
|
|
677
605
|
const th = discord.thread(thread.id);
|
|
678
606
|
const firstReply = await th.waitForBotReply({ timeout: 4_000 });
|
|
679
|
-
expect(firstReply.
|
|
607
|
+
expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0);
|
|
680
608
|
// Ensure the setup run is fully settled before slash-queue checks.
|
|
681
609
|
// Otherwise the first /queue call can race with a still-busy run window.
|
|
682
610
|
await waitForFooterMessage({
|
|
@@ -746,7 +674,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
746
674
|
await waitForBotMessageContaining({
|
|
747
675
|
discord,
|
|
748
676
|
threadId: thread.id,
|
|
749
|
-
text: '
|
|
677
|
+
text: 'ok',
|
|
750
678
|
afterMessageId: dispatchIndicatorMessage.id,
|
|
751
679
|
timeout: 8_000,
|
|
752
680
|
});
|
|
@@ -754,23 +682,23 @@ e2eTest('thread message queue ordering', () => {
|
|
|
754
682
|
discord,
|
|
755
683
|
threadId: thread.id,
|
|
756
684
|
timeout: 8_000,
|
|
757
|
-
afterMessageIncludes: '
|
|
685
|
+
afterMessageIncludes: 'ok',
|
|
758
686
|
afterAuthorId: discord.botUserId,
|
|
759
687
|
});
|
|
760
688
|
expect(await th.text()).toMatchInlineSnapshot(`
|
|
761
689
|
"--- from: user (queue-tester)
|
|
762
690
|
Reply with exactly: queue-slash-setup
|
|
763
691
|
--- from: assistant (TestBot)
|
|
764
|
-
*using deterministic-provider/deterministic-v2*
|
|
765
|
-
|
|
766
|
-
*project ⋅ main ⋅
|
|
692
|
+
> *using deterministic-provider/deterministic-v2*
|
|
693
|
+
> ok
|
|
694
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
|
|
767
695
|
» **queue-tester:** Reply with exactly: race-final
|
|
768
696
|
Queued message (position 1)
|
|
769
|
-
|
|
770
|
-
*project ⋅ main ⋅
|
|
697
|
+
> race-final
|
|
698
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
|
|
771
699
|
» **queue-tester:** Reply with exactly: queued-from-slash
|
|
772
|
-
|
|
773
|
-
*project ⋅ main ⋅
|
|
700
|
+
> ok
|
|
701
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
|
|
774
702
|
`);
|
|
775
703
|
}, 12_000);
|
|
776
704
|
test('Remove from queue button clears only that queued message', async () => {
|
|
@@ -864,7 +792,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
864
792
|
discord,
|
|
865
793
|
threadId: thread.id,
|
|
866
794
|
timeout: 8_000,
|
|
867
|
-
afterMessageIncludes: '
|
|
795
|
+
afterMessageIncludes: 'ok',
|
|
868
796
|
afterAuthorId: discord.botUserId,
|
|
869
797
|
});
|
|
870
798
|
const threadText = await th.text();
|
|
@@ -872,96 +800,19 @@ e2eTest('thread message queue ordering', () => {
|
|
|
872
800
|
"--- from: user (queue-tester)
|
|
873
801
|
Reply with exactly: clear-queue-setup
|
|
874
802
|
--- from: assistant (TestBot)
|
|
875
|
-
*using deterministic-provider/deterministic-v2*
|
|
876
|
-
|
|
877
|
-
*project ⋅ main ⋅
|
|
803
|
+
> *using deterministic-provider/deterministic-v2*
|
|
804
|
+
> ok
|
|
805
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
|
|
878
806
|
» **queue-tester:** Reply with exactly: race-final
|
|
879
807
|
Removed queued message (was position 1): Reply with exactly: removed-queued-message
|
|
880
808
|
Queued message (position 2)
|
|
881
|
-
|
|
882
|
-
*project ⋅ main ⋅
|
|
809
|
+
> race-final
|
|
810
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
|
|
883
811
|
» **queue-tester:** Reply with exactly: kept-queued-message"
|
|
884
812
|
`);
|
|
885
813
|
expect(threadText).not.toContain('» **queue-tester:** Reply with exactly: removed-queued-message');
|
|
886
814
|
expect(threadText).toContain('kept-queued-message');
|
|
887
815
|
}, 12_000);
|
|
888
|
-
test('queued message waits for running session and then processes next', async () => {
|
|
889
|
-
// When a new message arrives while a session is running, it queues and
|
|
890
|
-
// runs after the in-flight request completes.
|
|
891
|
-
//
|
|
892
|
-
// 1. Fast setup: establish session
|
|
893
|
-
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
894
|
-
content: 'Reply with exactly: delta',
|
|
895
|
-
});
|
|
896
|
-
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
897
|
-
timeout: 4_000,
|
|
898
|
-
predicate: (t) => {
|
|
899
|
-
return t.name === 'Reply with exactly: delta';
|
|
900
|
-
},
|
|
901
|
-
});
|
|
902
|
-
const th = discord.thread(thread.id);
|
|
903
|
-
const firstReply = await th.waitForBotReply({ timeout: 4_000 });
|
|
904
|
-
expect(firstReply.content.trim().length).toBeGreaterThan(0);
|
|
905
|
-
const before = await th.getMessages();
|
|
906
|
-
const beforeBotCount = before.filter((m) => {
|
|
907
|
-
return m.author.id === discord.botUserId;
|
|
908
|
-
}).length;
|
|
909
|
-
// 2. Send B, then quickly send C to enqueue behind B.
|
|
910
|
-
await th.user(TEST_USER_ID).sendMessage({
|
|
911
|
-
content: 'Reply with exactly: echo',
|
|
912
|
-
});
|
|
913
|
-
await new Promise((r) => {
|
|
914
|
-
setTimeout(r, 500);
|
|
915
|
-
});
|
|
916
|
-
await th.user(TEST_USER_ID).sendMessage({
|
|
917
|
-
content: 'Reply with exactly: foxtrot',
|
|
918
|
-
});
|
|
919
|
-
// 3. Poll until foxtrot's user message has a bot reply after it.
|
|
920
|
-
// waitForBotMessageCount alone isn't enough — error messages from the
|
|
921
|
-
// interrupted session can satisfy the count before foxtrot gets its reply.
|
|
922
|
-
const after = await waitForBotReplyAfterUserMessage({
|
|
923
|
-
discord,
|
|
924
|
-
threadId: thread.id,
|
|
925
|
-
userId: TEST_USER_ID,
|
|
926
|
-
userMessageIncludes: 'foxtrot',
|
|
927
|
-
timeout: 4_000,
|
|
928
|
-
});
|
|
929
|
-
// 4. Foxtrot got a bot response after B/C were processed.
|
|
930
|
-
const afterBotMessages = after.filter((m) => {
|
|
931
|
-
return m.author.id === discord.botUserId;
|
|
932
|
-
});
|
|
933
|
-
expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1);
|
|
934
|
-
await waitForFooterMessage({
|
|
935
|
-
discord,
|
|
936
|
-
threadId: thread.id,
|
|
937
|
-
timeout: 4_000,
|
|
938
|
-
afterMessageIncludes: 'foxtrot',
|
|
939
|
-
afterAuthorId: TEST_USER_ID,
|
|
940
|
-
});
|
|
941
|
-
// Assert ordering invariants instead of exact snapshot — the echo reply
|
|
942
|
-
// and footer can interleave non-deterministically on slower CI hardware.
|
|
943
|
-
const finalMessages = await th.getMessages();
|
|
944
|
-
const userEchoIndex = finalMessages.findIndex((m) => {
|
|
945
|
-
return m.author.id === TEST_USER_ID && m.content.includes('echo');
|
|
946
|
-
});
|
|
947
|
-
const userFoxtrotIndex = finalMessages.findIndex((m) => {
|
|
948
|
-
return m.author.id === TEST_USER_ID && m.content.includes('foxtrot');
|
|
949
|
-
});
|
|
950
|
-
expect(userEchoIndex).toBeGreaterThan(-1);
|
|
951
|
-
expect(userFoxtrotIndex).toBeGreaterThan(-1);
|
|
952
|
-
// User messages appear in send order
|
|
953
|
-
expect(userEchoIndex).toBeLessThan(userFoxtrotIndex);
|
|
954
|
-
// Foxtrot's bot reply appears after the foxtrot user message
|
|
955
|
-
const botAfterFoxtrot = finalMessages.findIndex((m, i) => {
|
|
956
|
-
return i > userFoxtrotIndex && m.author.id === discord.botUserId;
|
|
957
|
-
});
|
|
958
|
-
expect(botAfterFoxtrot).toBeGreaterThan(userFoxtrotIndex);
|
|
959
|
-
// A footer appears after foxtrot (session completed)
|
|
960
|
-
const timeline = await th.text();
|
|
961
|
-
expect(timeline).toContain('Reply with exactly: echo');
|
|
962
|
-
expect(timeline).toContain('Reply with exactly: foxtrot');
|
|
963
|
-
expect(timeline).toContain('*project ⋅ main ⋅');
|
|
964
|
-
}, 8_000);
|
|
965
816
|
test('slow stream still processes queued next message after completion', async () => {
|
|
966
817
|
// A message sent mid-stream queues and runs after the in-flight request
|
|
967
818
|
// completes (no auto-interrupt).
|
|
@@ -977,7 +828,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
977
828
|
});
|
|
978
829
|
const th = discord.thread(thread.id);
|
|
979
830
|
const firstReply = await th.waitForBotReply({ timeout: 4_000 });
|
|
980
|
-
expect(firstReply.
|
|
831
|
+
expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0);
|
|
981
832
|
// Wait for golf's footer so the golf→hotel transition is deterministic
|
|
982
833
|
await waitForFooterMessage({
|
|
983
834
|
discord,
|
|
@@ -1017,14 +868,14 @@ e2eTest('thread message queue ordering', () => {
|
|
|
1017
868
|
"--- from: user (queue-tester)
|
|
1018
869
|
Reply with exactly: golf
|
|
1019
870
|
--- from: assistant (TestBot)
|
|
1020
|
-
*using deterministic-provider/deterministic-v2*
|
|
1021
|
-
|
|
1022
|
-
*project ⋅ main ⋅
|
|
871
|
+
> *using deterministic-provider/deterministic-v2*
|
|
872
|
+
> ok
|
|
873
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
|
|
1023
874
|
--- from: user (queue-tester)
|
|
1024
875
|
Reply with exactly: hotel
|
|
1025
876
|
Reply with exactly: india
|
|
1026
877
|
--- from: assistant (TestBot)
|
|
1027
|
-
|
|
878
|
+
> ok"
|
|
1028
879
|
`);
|
|
1029
880
|
const userIndiaIndex = after.findIndex((m) => {
|
|
1030
881
|
return m.author.id === TEST_USER_ID && m.content.includes('india');
|
|
@@ -1049,7 +900,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
1049
900
|
});
|
|
1050
901
|
const th = discord.thread(thread.id);
|
|
1051
902
|
const firstReply = await th.waitForBotReply({ timeout: 4_000 });
|
|
1052
|
-
expect(firstReply.
|
|
903
|
+
expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0);
|
|
1053
904
|
const before = await th.getMessages();
|
|
1054
905
|
const beforeBotCount = before.filter((m) => {
|
|
1055
906
|
return m.author.id === discord.botUserId;
|
|
@@ -1087,12 +938,12 @@ e2eTest('thread message queue ordering', () => {
|
|
|
1087
938
|
const textWithoutFooters = (await th.text())
|
|
1088
939
|
.split('\n')
|
|
1089
940
|
.filter((line) => {
|
|
1090
|
-
return !line.startsWith('*project ⋅');
|
|
941
|
+
return !line.startsWith('*project ⋅') && !line.startsWith('> *project ⋅');
|
|
1091
942
|
})
|
|
1092
943
|
.join('\n');
|
|
1093
944
|
const normalizedTextWithoutFooters = textWithoutFooters.replace([
|
|
1094
945
|
'--- from: assistant (TestBot)',
|
|
1095
|
-
'
|
|
946
|
+
'ok',
|
|
1096
947
|
'--- from: user (queue-tester)',
|
|
1097
948
|
'Reply with exactly: november',
|
|
1098
949
|
].join('\n'), [
|
|
@@ -1104,16 +955,17 @@ e2eTest('thread message queue ordering', () => {
|
|
|
1104
955
|
"--- from: user (queue-tester)
|
|
1105
956
|
Reply with exactly: juliet
|
|
1106
957
|
--- from: assistant (TestBot)
|
|
1107
|
-
*using deterministic-provider/deterministic-v2*
|
|
958
|
+
> *using deterministic-provider/deterministic-v2*
|
|
1108
959
|
--- from: user (queue-tester)
|
|
1109
960
|
Reply with exactly: kilo
|
|
1110
961
|
Reply with exactly: lima
|
|
1111
962
|
Reply with exactly: mike
|
|
1112
963
|
--- from: assistant (TestBot)
|
|
964
|
+
> ok
|
|
1113
965
|
--- from: user (queue-tester)
|
|
1114
966
|
Reply with exactly: november
|
|
1115
967
|
--- from: assistant (TestBot)
|
|
1116
|
-
|
|
968
|
+
> ok"
|
|
1117
969
|
`);
|
|
1118
970
|
// E's user message appears before the final bot response
|
|
1119
971
|
const userNovemberIndex = afterE.findIndex((m) => {
|
|
@@ -1198,17 +1050,17 @@ e2eTest('thread message queue ordering', () => {
|
|
|
1198
1050
|
"--- from: user (queue-tester)
|
|
1199
1051
|
SLOW_BUSY_MARKER Reply with exactly: edit-queue-setup
|
|
1200
1052
|
--- from: assistant (TestBot)
|
|
1201
|
-
*using deterministic-provider/deterministic-v2*
|
|
1053
|
+
> *using deterministic-provider/deterministic-v2*
|
|
1202
1054
|
--- from: user (queue-tester)
|
|
1203
1055
|
Reply with exactly: edited-queued. queue
|
|
1204
1056
|
--- from: assistant (TestBot)
|
|
1205
1057
|
Queued at position 1. Edit or delete your message to update the queue
|
|
1206
1058
|
⬦ **queue-tester** edited queued message
|
|
1207
|
-
|
|
1208
|
-
*project ⋅ main ⋅
|
|
1059
|
+
> slow-busy-reply
|
|
1060
|
+
> *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2*
|
|
1209
1061
|
» **queue-tester:** Reply with exactly: edited-queued
|
|
1210
|
-
|
|
1211
|
-
*project ⋅ main ⋅
|
|
1062
|
+
> ok
|
|
1063
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
|
|
1212
1064
|
`);
|
|
1213
1065
|
const finalText = await th.text();
|
|
1214
1066
|
expect(finalText).toContain('edited-queued');
|
|
@@ -1291,14 +1143,14 @@ e2eTest('thread message queue ordering', () => {
|
|
|
1291
1143
|
"--- from: user (queue-tester)
|
|
1292
1144
|
SLOW_BUSY_MARKER Reply with exactly: remove-queue-setup
|
|
1293
1145
|
--- from: assistant (TestBot)
|
|
1294
|
-
*using deterministic-provider/deterministic-v2*
|
|
1146
|
+
> *using deterministic-provider/deterministic-v2*
|
|
1295
1147
|
--- from: user (queue-tester)
|
|
1296
1148
|
Reply with exactly: will-be-removed
|
|
1297
1149
|
--- from: assistant (TestBot)
|
|
1298
1150
|
Queued at position 1. Edit or delete your message to update the queue
|
|
1299
1151
|
⬦ **queue-tester** removed message from queue
|
|
1300
|
-
|
|
1301
|
-
*project ⋅ main ⋅
|
|
1152
|
+
> slow-busy-reply
|
|
1153
|
+
> *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
|
|
1302
1154
|
`);
|
|
1303
1155
|
}, 12_000);
|
|
1304
1156
|
});
|
|
@@ -19,7 +19,7 @@ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-set
|
|
|
19
19
|
import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
|
|
20
20
|
import { getThreadSession } from './database.js';
|
|
21
21
|
import { initializeOpencodeForDirectory } from './opencode.js';
|
|
22
|
-
const TEXT_CHANNEL_ID = '
|
|
22
|
+
const TEXT_CHANNEL_ID = '200000000000003205';
|
|
23
23
|
const e2eTest = describe;
|
|
24
24
|
e2eTest('/undo sets revert state and cleans up on next prompt', () => {
|
|
25
25
|
const ctx = setupQueueAdvancedSuite({
|
|
@@ -46,7 +46,13 @@ e2eTest('/undo sets revert state and cleans up on next prompt', () => {
|
|
|
46
46
|
},
|
|
47
47
|
});
|
|
48
48
|
const th = ctx.discord.thread(thread.id);
|
|
49
|
-
await
|
|
49
|
+
await waitForBotMessageContaining({
|
|
50
|
+
discord: ctx.discord,
|
|
51
|
+
threadId: thread.id,
|
|
52
|
+
userId: TEST_USER_ID,
|
|
53
|
+
text: 'creating undo file',
|
|
54
|
+
timeout: 8_000,
|
|
55
|
+
});
|
|
50
56
|
await waitForFooterMessage({
|
|
51
57
|
discord: ctx.discord,
|
|
52
58
|
threadId: thread.id,
|
|
@@ -147,17 +153,19 @@ e2eTest('/undo sets revert state and cleans up on next prompt', () => {
|
|
|
147
153
|
"--- from: user (undo-tester)
|
|
148
154
|
UNDO_FILE_MARKER
|
|
149
155
|
--- from: assistant (TestBot)
|
|
150
|
-
*using deterministic-provider/deterministic-v2*
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
> *using deterministic-provider/deterministic-v2*
|
|
157
|
+
> creating undo file
|
|
158
|
+
|
|
159
|
+
┣ bash _Create undo marker file_
|
|
160
|
+
|
|
161
|
+
> undo file created
|
|
162
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
|
|
155
163
|
Undone - reverted last assistant message
|
|
156
164
|
--- from: user (undo-tester)
|
|
157
165
|
Reply with exactly: after-undo-message
|
|
158
166
|
--- from: assistant (TestBot)
|
|
159
|
-
|
|
160
|
-
*project ⋅ main ⋅
|
|
167
|
+
> ok
|
|
168
|
+
> *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
|
|
161
169
|
`);
|
|
162
170
|
}, 20_000);
|
|
163
171
|
});
|
package/dist/voice-attachment.js
CHANGED
|
@@ -2,17 +2,39 @@
|
|
|
2
2
|
// Normalizes Discord attachment heuristics for voice-message detection so
|
|
3
3
|
// message routing, transcription, and empty-prompt guards all agree even when
|
|
4
4
|
// Discord omits contentType on uploaded audio attachments.
|
|
5
|
+
// iOS videos also have duration_secs; reject those before the duration heuristic.
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
const VOICE_ATTACHMENT_EXTENSIONS = new Set([
|
|
7
8
|
'.m4a',
|
|
8
9
|
'.mp3',
|
|
9
|
-
'.mp4',
|
|
10
10
|
'.oga',
|
|
11
11
|
'.ogg',
|
|
12
12
|
'.opus',
|
|
13
13
|
'.wav',
|
|
14
14
|
]);
|
|
15
|
+
const VIDEO_ATTACHMENT_EXTENSIONS = new Set([
|
|
16
|
+
'.avi',
|
|
17
|
+
'.m4v',
|
|
18
|
+
'.mkv',
|
|
19
|
+
'.mov',
|
|
20
|
+
'.mp4',
|
|
21
|
+
'.webm',
|
|
22
|
+
]);
|
|
23
|
+
function isVisualMediaAttachment(attachment) {
|
|
24
|
+
const contentType = attachment.contentType?.trim().toLowerCase() || '';
|
|
25
|
+
if (contentType.startsWith('video/'))
|
|
26
|
+
return true;
|
|
27
|
+
const extension = path.extname(attachment.name || '').toLowerCase();
|
|
28
|
+
if (VIDEO_ATTACHMENT_EXTENSIONS.has(extension))
|
|
29
|
+
return true;
|
|
30
|
+
return (typeof attachment.width === 'number' &&
|
|
31
|
+
attachment.width > 0 &&
|
|
32
|
+
typeof attachment.height === 'number' &&
|
|
33
|
+
attachment.height > 0);
|
|
34
|
+
}
|
|
15
35
|
export function getVoiceAttachmentMatchReason(attachment) {
|
|
36
|
+
if (isVisualMediaAttachment(attachment))
|
|
37
|
+
return null;
|
|
16
38
|
const contentType = attachment.contentType?.trim().toLowerCase() || '';
|
|
17
39
|
if (contentType.startsWith('audio/')) {
|
|
18
40
|
return `contentType:${contentType}`;
|
package/dist/voice-handler.js
CHANGED
|
@@ -16,7 +16,7 @@ import { transcribeViaKimakiGateway } from './cloudflare-transcription.js';
|
|
|
16
16
|
import { sendThreadMessage, escapeDiscordFormatting, NOTIFY_MESSAGE_FLAGS, hasKimakiBotPermission, } from './discord-utils.js';
|
|
17
17
|
import { requestAudioApiKey } from './commands/gemini-apikey.js';
|
|
18
18
|
import { transcribeAudio } from './voice.js';
|
|
19
|
-
import { DiscordOperationError, FetchError } from './errors.js';
|
|
19
|
+
import { DiscordOperationError, FetchError, EmptyTranscriptionError, TranscriptionError } from './errors.js';
|
|
20
20
|
import { store } from './store.js';
|
|
21
21
|
import { getVoiceAttachmentMatchReason, isVoiceAttachment, } from './voice-attachment.js';
|
|
22
22
|
import { execAsync } from './worktrees.js';
|
|
@@ -352,7 +352,7 @@ export async function cleanupVoiceConnection(guildId) {
|
|
|
352
352
|
}
|
|
353
353
|
// Per-thread serialization is handled by ThreadSessionRuntime.enqueueIncoming()
|
|
354
354
|
// via the runtime action queue; no local serialization is needed here.
|
|
355
|
-
export async function processVoiceAttachment({ message, thread, projectDirectory, isNewThread = false, appId, currentSessionContext, lastSessionContext, agents, }) {
|
|
355
|
+
export async function processVoiceAttachment({ message, thread, projectDirectory, isNewThread = false, appId, currentSessionContext, lastSessionContext, agents, canForkSession = false, }) {
|
|
356
356
|
const audioAttachment = Array.from(message.attachments.values()).find((attachment) => isVoiceAttachment(attachment));
|
|
357
357
|
if (!audioAttachment)
|
|
358
358
|
return null;
|
|
@@ -387,7 +387,12 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
|
|
|
387
387
|
transcription: deterministicConfig.transcription,
|
|
388
388
|
queueMessage: deterministicConfig.queueMessage,
|
|
389
389
|
agent: deterministicConfig.agent,
|
|
390
|
+
sessionAction: deterministicConfig.sessionAction,
|
|
390
391
|
};
|
|
392
|
+
if (!result.transcription.trim()) {
|
|
393
|
+
await sendThreadMessage(thread, 'No request was transcribed. Record another voice message with the task to send.');
|
|
394
|
+
return new EmptyTranscriptionError();
|
|
395
|
+
}
|
|
391
396
|
voiceLogger.log(`[DETERMINISTIC] Returning canned transcription: "${result.transcription}"${result.queueMessage ? ' [QUEUE]' : ''}`);
|
|
392
397
|
if (isNewThread) {
|
|
393
398
|
const threadName = result.transcription.replace(/\s+/g, ' ').trim().slice(0, 80);
|
|
@@ -402,6 +407,10 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
|
|
|
402
407
|
}
|
|
403
408
|
}
|
|
404
409
|
await sendThreadMessage(thread, `📝 **Transcribed message:** ${escapeDiscordFormatting(result.transcription)}`);
|
|
410
|
+
if (result.sessionAction === 'btw' && !canForkSession) {
|
|
411
|
+
await sendThreadMessage(thread, 'A contextual side chat needs an existing session. Nothing was sent to the agent. Retry from a session, or ask to create a fresh chat with this request.');
|
|
412
|
+
return new TranscriptionError({ reason: 'Contextual routing is unavailable without a source session' });
|
|
413
|
+
}
|
|
405
414
|
return result;
|
|
406
415
|
}
|
|
407
416
|
const audioResponse = await fetch(audioAttachment.url)
|
|
@@ -450,7 +459,7 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
|
|
|
450
459
|
// No user-configured key: gateway-mode installs get one free transcription
|
|
451
460
|
// via the kimaki.dev Whisper fallback before we bother the user with the
|
|
452
461
|
// "add API key" dialog. This fallback has no tool-calling, so it can't
|
|
453
|
-
// detect queueMessage/agent hints spoken in the voice message the way the
|
|
462
|
+
// detect queueMessage/sessionAction/agent hints spoken in the voice message the way the
|
|
454
463
|
// OpenAI/Gemini path can.
|
|
455
464
|
let gatewayTranscription;
|
|
456
465
|
if (!transcriptionApiKey) {
|
|
@@ -496,13 +505,17 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
|
|
|
496
505
|
currentSessionContext,
|
|
497
506
|
lastSessionContext,
|
|
498
507
|
agents,
|
|
508
|
+
canForkSession,
|
|
499
509
|
});
|
|
500
510
|
if (transcription instanceof Error) {
|
|
511
|
+
if (transcription instanceof EmptyTranscriptionError) {
|
|
512
|
+
await sendThreadMessage(thread, 'No request was transcribed. Record another voice message with the task to send.');
|
|
513
|
+
return transcription;
|
|
514
|
+
}
|
|
501
515
|
const errMsg = errore.matchError(transcription, {
|
|
502
516
|
ApiKeyMissingError: (e) => e.message,
|
|
503
517
|
InvalidAudioFormatError: (e) => e.message,
|
|
504
518
|
TranscriptionError: (e) => e.message,
|
|
505
|
-
EmptyTranscriptionError: (e) => e.message,
|
|
506
519
|
NoResponseContentError: (e) => e.message,
|
|
507
520
|
NoToolResponseError: (e) => e.message,
|
|
508
521
|
Error: (e) => e.message,
|
|
@@ -539,6 +552,10 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
|
|
|
539
552
|
}
|
|
540
553
|
}
|
|
541
554
|
await sendThreadMessage(thread, `📝 **Transcribed message:** ${escapeDiscordFormatting(text)}`);
|
|
555
|
+
if (transcription.sessionAction === 'btw' && !canForkSession) {
|
|
556
|
+
await sendThreadMessage(thread, 'A contextual side chat needs an existing session. Nothing was sent to the agent. Retry from a session, or ask to create a fresh chat with this request.');
|
|
557
|
+
return new TranscriptionError({ reason: 'Contextual routing is unavailable without a source session' });
|
|
558
|
+
}
|
|
542
559
|
if (agent) {
|
|
543
560
|
await sendThreadMessage(thread, `Detected agent: ${agent}`);
|
|
544
561
|
}
|