kimaki 0.4.82 → 0.4.84
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/LICENSE +21 -0
- package/dist/anthropic-auth-plugin.js +7 -0
- package/dist/cli.js +51 -7
- package/dist/commands/abort.js +5 -16
- package/dist/commands/action-buttons.js +3 -3
- package/dist/commands/add-project.js +1 -1
- package/dist/commands/ask-question.js +3 -3
- package/dist/commands/context-usage.js +1 -1
- package/dist/commands/create-new-project.js +1 -1
- package/dist/commands/fork.js +11 -8
- package/dist/commands/merge-worktree.js +1 -1
- package/dist/commands/new-worktree.js +63 -44
- package/dist/commands/remove-project.js +1 -1
- package/dist/commands/resume.js +11 -8
- package/dist/commands/screenshare.js +14 -6
- package/dist/commands/screenshare.test.js +20 -0
- package/dist/commands/session.js +1 -1
- package/dist/commands/undo-redo.js +91 -7
- package/dist/commands/user-command.js +1 -1
- package/dist/config.js +16 -1
- package/dist/database.js +53 -2
- package/dist/db.js +6 -0
- package/dist/discord-bot.js +48 -85
- package/dist/discord-command-registration.js +1 -1
- package/dist/external-opencode-sync.js +515 -0
- package/dist/external-opencode-sync.test.js +151 -0
- package/dist/gateway-proxy.e2e.test.js +8 -5
- package/dist/genai.js +1 -1
- package/dist/generated/enums.js +4 -0
- package/dist/generated/internal/class.js +4 -4
- package/dist/generated/internal/prismaNamespace.js +1 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
- package/dist/generated/models/external_session_pending_prompts.js +1 -0
- package/dist/hrana-server.js +14 -285
- package/dist/hrana-server.test.js +4 -2
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +7 -0
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/kitty-graphics-parser.js +3 -0
- package/dist/kitty-graphics-parser.test.js +276 -0
- package/dist/kitty-graphics-plugin.js +3 -0
- package/dist/markdown.js +4 -4
- package/dist/markdown.test.js +1 -1
- package/dist/message-formatting.js +54 -15
- package/dist/onboarding-tutorial.js +1 -1
- package/dist/openai-realtime.js +9 -13
- package/dist/opencode.js +28 -5
- package/dist/queue-advanced-e2e-setup.js +89 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +5 -5
- package/dist/queue-advanced-typing.e2e.test.js +9 -22
- package/dist/queue-question-select-drain.e2e.test.js +117 -0
- package/dist/session-handler/event-stream-state.js +101 -7
- package/dist/session-handler/event-stream-state.test.js +7 -3
- package/dist/session-handler/thread-session-runtime.js +120 -9
- package/dist/store.js +1 -0
- package/dist/system-message.js +22 -4
- package/dist/system-message.test.js +19 -0
- package/dist/task-runner.js +1 -1
- package/dist/thread-message-queue.e2e.test.js +8 -14
- package/dist/tools.js +1 -1
- package/dist/undo-redo.e2e.test.js +20 -25
- package/package.json +10 -6
- package/schema.prisma +6 -0
- package/skills/errore/SKILL.md +40 -13
- package/skills/goke/SKILL.md +12 -0
- package/skills/lintcn/SKILL.md +868 -0
- package/skills/npm-package/SKILL.md +1 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/spiceflow/SKILL.md +1 -1
- package/skills/usecomputer/SKILL.md +339 -0
- package/src/ai-tool-to-genai.ts +1 -0
- package/src/anthropic-auth-plugin.ts +7 -0
- package/src/cli.ts +59 -6
- package/src/commands/abort.ts +6 -16
- package/src/commands/action-buttons.ts +5 -1
- package/src/commands/add-project.ts +1 -1
- package/src/commands/ask-question.ts +5 -2
- package/src/commands/context-usage.ts +1 -1
- package/src/commands/create-new-project.ts +1 -1
- package/src/commands/fork.ts +12 -11
- package/src/commands/merge-worktree.ts +1 -1
- package/src/commands/new-worktree.ts +74 -55
- package/src/commands/remove-project.ts +1 -1
- package/src/commands/resume.ts +12 -10
- package/src/commands/screenshare.test.ts +30 -0
- package/src/commands/screenshare.ts +18 -6
- package/src/commands/session.ts +1 -1
- package/src/commands/undo-redo.ts +108 -10
- package/src/commands/user-command.ts +1 -1
- package/src/config.ts +19 -1
- package/src/database.ts +72 -3
- package/src/db.ts +8 -0
- package/src/discord-bot.ts +58 -93
- package/src/discord-command-registration.ts +1 -1
- package/src/external-opencode-sync.ts +729 -0
- package/src/gateway-proxy.e2e.test.ts +9 -5
- package/src/genai.ts +3 -3
- package/src/generated/commonInputTypes.ts +34 -0
- package/src/generated/enums.ts +8 -0
- package/src/generated/internal/class.ts +4 -4
- package/src/generated/internal/prismaNamespace.ts +8 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
- package/src/generated/models/thread_sessions.ts +53 -1
- package/src/hrana-server.test.ts +8 -2
- package/src/hrana-server.ts +18 -390
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +7 -0
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/markdown.test.ts +1 -1
- package/src/markdown.ts +4 -4
- package/src/message-formatting.ts +66 -17
- package/src/onboarding-tutorial.ts +1 -1
- package/src/openai-realtime.ts +6 -10
- package/src/opencode.ts +31 -7
- package/src/queue-advanced-e2e-setup.ts +92 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +5 -5
- package/src/queue-advanced-typing.e2e.test.ts +9 -22
- package/src/queue-question-select-drain.e2e.test.ts +149 -0
- package/src/schema.sql +1 -0
- package/src/session-handler/event-stream-state.test.ts +7 -2
- package/src/session-handler/event-stream-state.ts +128 -7
- package/src/session-handler/thread-runtime-state.ts +5 -0
- package/src/session-handler/thread-session-runtime.ts +153 -11
- package/src/store.ts +8 -0
- package/src/system-message.ts +27 -4
- package/src/task-runner.ts +1 -1
- package/src/thread-message-queue.e2e.test.ts +8 -14
- package/src/tools.ts +1 -1
- package/src/undo-redo.e2e.test.ts +28 -26
- package/skills/jitter/node_modules/.bin/esbuild +0 -21
- package/skills/jitter/node_modules/.bin/tsc +0 -21
- package/skills/jitter/node_modules/.bin/tsserver +0 -21
- package/skills/jitter/node_modules/typescript/LICENSE.txt +0 -55
- package/skills/jitter/node_modules/typescript/README.md +0 -50
- package/skills/jitter/node_modules/typescript/SECURITY.md +0 -41
- package/skills/jitter/node_modules/typescript/ThirdPartyNoticeText.txt +0 -193
- package/skills/jitter/node_modules/typescript/bin/tsc +0 -2
- package/skills/jitter/node_modules/typescript/bin/tsserver +0 -2
- package/skills/jitter/node_modules/typescript/lib/_tsc.js +0 -133792
- package/skills/jitter/node_modules/typescript/lib/_tsserver.js +0 -659
- package/skills/jitter/node_modules/typescript/lib/_typingsInstaller.js +0 -222
- package/skills/jitter/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/lib.d.ts +0 -22
- package/skills/jitter/node_modules/typescript/lib/lib.decorators.d.ts +0 -384
- package/skills/jitter/node_modules/typescript/lib/lib.decorators.legacy.d.ts +0 -22
- package/skills/jitter/node_modules/typescript/lib/lib.dom.asynciterable.d.ts +0 -41
- package/skills/jitter/node_modules/typescript/lib/lib.dom.d.ts +0 -39429
- package/skills/jitter/node_modules/typescript/lib/lib.dom.iterable.d.ts +0 -571
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.collection.d.ts +0 -147
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.core.d.ts +0 -597
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.d.ts +0 -28
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.generator.d.ts +0 -77
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.iterable.d.ts +0 -605
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.promise.d.ts +0 -81
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.proxy.d.ts +0 -128
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.reflect.d.ts +0 -144
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.d.ts +0 -46
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts +0 -326
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.array.include.d.ts +0 -116
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.full.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.intl.d.ts +0 -31
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.d.ts +0 -26
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.date.d.ts +0 -31
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.full.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.intl.d.ts +0 -44
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.object.d.ts +0 -49
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts +0 -135
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.string.d.ts +0 -45
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts +0 -53
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts +0 -77
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts +0 -53
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.intl.d.ts +0 -83
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.promise.d.ts +0 -30
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.regexp.d.ts +0 -37
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.array.d.ts +0 -79
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.intl.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.object.d.ts +0 -33
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.string.d.ts +0 -37
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.symbol.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.bigint.d.ts +0 -765
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.d.ts +0 -27
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.date.d.ts +0 -42
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.intl.d.ts +0 -474
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.number.d.ts +0 -28
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.promise.d.ts +0 -47
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts +0 -99
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.string.d.ts +0 -44
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts +0 -41
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.intl.d.ts +0 -166
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.promise.d.ts +0 -48
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.string.d.ts +0 -33
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.weakref.d.ts +0 -78
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.array.d.ts +0 -121
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.error.d.ts +0 -75
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.intl.d.ts +0 -145
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.object.d.ts +0 -26
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.regexp.d.ts +0 -39
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.string.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.array.d.ts +0 -924
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.collection.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.d.ts +0 -22
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.intl.d.ts +0 -56
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts +0 -65
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.collection.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.d.ts +0 -26
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.object.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.promise.d.ts +0 -35
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.regexp.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts +0 -68
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.string.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.es5.d.ts +0 -4601
- package/skills/jitter/node_modules/typescript/lib/lib.es6.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.array.d.ts +0 -35
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.collection.d.ts +0 -96
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.decorators.d.ts +0 -28
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.disposable.d.ts +0 -193
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.error.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.float16.d.ts +0 -443
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.intl.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.iterator.d.ts +0 -148
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.promise.d.ts +0 -34
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.scripthost.d.ts +0 -322
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.asynciterable.d.ts +0 -41
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.d.ts +0 -13150
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.importscripts.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.iterable.d.ts +0 -340
- package/skills/jitter/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/tsc.js +0 -8
- package/skills/jitter/node_modules/typescript/lib/tsserver.js +0 -8
- package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.d.ts +0 -17
- package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.js +0 -21
- package/skills/jitter/node_modules/typescript/lib/typesMap.json +0 -497
- package/skills/jitter/node_modules/typescript/lib/typescript.d.ts +0 -11438
- package/skills/jitter/node_modules/typescript/lib/typescript.js +0 -200253
- package/skills/jitter/node_modules/typescript/lib/typingsInstaller.js +0 -8
- package/skills/jitter/node_modules/typescript/lib/watchGuard.js +0 -53
- package/skills/jitter/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/node_modules/.bin/tsc +0 -21
- package/skills/jitter/node_modules/typescript/node_modules/.bin/tsserver +0 -21
- package/skills/jitter/node_modules/typescript/package.json +0 -120
package/src/opencode.ts
CHANGED
|
@@ -67,6 +67,10 @@ import {
|
|
|
67
67
|
|
|
68
68
|
const opencodeLogger = createLogger(LogPrefix.OPENCODE)
|
|
69
69
|
|
|
70
|
+
// Tracks directories that have been initialized, to avoid repeated log spam
|
|
71
|
+
// from the external sync polling loop.
|
|
72
|
+
const initializedDirectories = new Set<string>()
|
|
73
|
+
|
|
70
74
|
const STARTUP_STDERR_TAIL_LIMIT = 30
|
|
71
75
|
const STARTUP_STDERR_LINE_MAX_LENGTH = 120
|
|
72
76
|
const STARTUP_ERROR_REASON_MAX_LENGTH = 1500
|
|
@@ -514,6 +518,20 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
514
518
|
opencodeLogger.warn(kimakiShimDirectory.message)
|
|
515
519
|
}
|
|
516
520
|
const gatewayToken = store.getState().gatewayToken
|
|
521
|
+
const vitestOpencodeEnv = (() => {
|
|
522
|
+
if (process.env.KIMAKI_VITEST !== '1') {
|
|
523
|
+
return {}
|
|
524
|
+
}
|
|
525
|
+
const root = path.join(getDataDir(), 'opencode-vitest-home')
|
|
526
|
+
return {
|
|
527
|
+
OPENCODE_TEST_HOME: root,
|
|
528
|
+
OPENCODE_CONFIG_DIR: path.join(root, '.opencode-kimaki'),
|
|
529
|
+
XDG_CONFIG_HOME: path.join(root, '.config'),
|
|
530
|
+
XDG_DATA_HOME: path.join(root, '.local', 'share'),
|
|
531
|
+
XDG_CACHE_HOME: path.join(root, '.cache'),
|
|
532
|
+
XDG_STATE_HOME: path.join(root, '.local', 'state'),
|
|
533
|
+
}
|
|
534
|
+
})()
|
|
517
535
|
|
|
518
536
|
const serverProcess = spawn(
|
|
519
537
|
spawnCommand,
|
|
@@ -574,6 +592,7 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
574
592
|
...(process.env.KIMAKI_SENTRY_DSN && {
|
|
575
593
|
KIMAKI_SENTRY_DSN: process.env.KIMAKI_SENTRY_DSN,
|
|
576
594
|
}),
|
|
595
|
+
...vitestOpencodeEnv,
|
|
577
596
|
...(pathEnv && { [pathEnvKey]: pathEnv }),
|
|
578
597
|
},
|
|
579
598
|
},
|
|
@@ -644,10 +663,12 @@ async function startSingleServer(): Promise<ServerStartError | SingleServer> {
|
|
|
644
663
|
clientCache.clear()
|
|
645
664
|
notifyServerLifecycle({ type: 'stopped' })
|
|
646
665
|
|
|
647
|
-
// Intentional kills
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
|
|
666
|
+
// Intentional kills should not trigger auto-restart:
|
|
667
|
+
// - SIGTERM from our cleanup/restart code
|
|
668
|
+
// - SIGINT propagated from Ctrl+C (parent process group signal)
|
|
669
|
+
// - any exit during bot shutdown (shuttingDown flag)
|
|
670
|
+
// Only unexpected crashes (non-zero exit without signal) get retried.
|
|
671
|
+
if (signal === 'SIGTERM' || signal === 'SIGINT' || (global as any).shuttingDown) {
|
|
651
672
|
serverRetryCount = 0
|
|
652
673
|
return
|
|
653
674
|
}
|
|
@@ -781,9 +802,12 @@ export async function initializeOpencodeForDirectory(
|
|
|
781
802
|
return server
|
|
782
803
|
}
|
|
783
804
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
805
|
+
if (!initializedDirectories.has(directory)) {
|
|
806
|
+
initializedDirectories.add(directory)
|
|
807
|
+
opencodeLogger.log(
|
|
808
|
+
`Using shared server on port ${server.port} for directory: ${directory}`,
|
|
809
|
+
)
|
|
810
|
+
}
|
|
787
811
|
|
|
788
812
|
return () => {
|
|
789
813
|
if (!singleServer) {
|
|
@@ -330,6 +330,42 @@ export function createDeterministicMatchers(): DeterministicMatcher[] {
|
|
|
330
330
|
},
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
+
// Question tool for select+queue drain test: model asks a question via dropdown,
|
|
334
|
+
// user answers via select menu while a message is queued.
|
|
335
|
+
const questionSelectQueueMatcher: DeterministicMatcher = {
|
|
336
|
+
id: 'question-select-queue-marker',
|
|
337
|
+
priority: 107,
|
|
338
|
+
when: {
|
|
339
|
+
lastMessageRole: 'user',
|
|
340
|
+
latestUserTextIncludes: 'QUESTION_SELECT_QUEUE_MARKER',
|
|
341
|
+
},
|
|
342
|
+
then: {
|
|
343
|
+
parts: [
|
|
344
|
+
{ type: 'stream-start', warnings: [] },
|
|
345
|
+
{
|
|
346
|
+
type: 'tool-call',
|
|
347
|
+
toolCallId: 'question-select-queue-call',
|
|
348
|
+
toolName: 'question',
|
|
349
|
+
input: JSON.stringify({
|
|
350
|
+
questions: [{
|
|
351
|
+
question: 'How to proceed?',
|
|
352
|
+
header: 'Select action',
|
|
353
|
+
options: [
|
|
354
|
+
{ label: 'Alpha', description: 'Alpha option' },
|
|
355
|
+
{ label: 'Beta', description: 'Beta option' },
|
|
356
|
+
],
|
|
357
|
+
}],
|
|
358
|
+
}),
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
type: 'finish',
|
|
362
|
+
finishReason: 'tool-calls',
|
|
363
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
}
|
|
368
|
+
|
|
333
369
|
// Model responds with text + tool call, then after tool result the
|
|
334
370
|
// follow-up matcher responds with text. This creates two assistant messages:
|
|
335
371
|
// first with finish="tool-calls" + completed, second with finish="stop".
|
|
@@ -389,6 +425,59 @@ export function createDeterministicMatchers(): DeterministicMatcher[] {
|
|
|
389
425
|
},
|
|
390
426
|
}
|
|
391
427
|
|
|
428
|
+
const undoFileMatcher: DeterministicMatcher = {
|
|
429
|
+
id: 'undo-file-marker',
|
|
430
|
+
priority: 111,
|
|
431
|
+
when: {
|
|
432
|
+
lastMessageRole: 'user',
|
|
433
|
+
latestUserTextIncludes: 'UNDO_FILE_MARKER',
|
|
434
|
+
},
|
|
435
|
+
then: {
|
|
436
|
+
parts: [
|
|
437
|
+
{ type: 'stream-start', warnings: [] },
|
|
438
|
+
{ type: 'text-start', id: 'undo-file-text' },
|
|
439
|
+
{ type: 'text-delta', id: 'undo-file-text', delta: 'creating undo file' },
|
|
440
|
+
{ type: 'text-end', id: 'undo-file-text' },
|
|
441
|
+
{
|
|
442
|
+
type: 'tool-call',
|
|
443
|
+
toolCallId: 'undo-file-bash',
|
|
444
|
+
toolName: 'bash',
|
|
445
|
+
input: JSON.stringify({
|
|
446
|
+
command: 'mkdir -p tmp && printf created > tmp/undo-marker.txt',
|
|
447
|
+
description: 'Create undo marker file',
|
|
448
|
+
}),
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
type: 'finish',
|
|
452
|
+
finishReason: 'tool-calls',
|
|
453
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const undoFileFollowupMatcher: DeterministicMatcher = {
|
|
460
|
+
id: 'undo-file-followup',
|
|
461
|
+
priority: 112,
|
|
462
|
+
when: {
|
|
463
|
+
latestUserTextIncludes: 'UNDO_FILE_MARKER',
|
|
464
|
+
rawPromptIncludes: 'creating undo file',
|
|
465
|
+
},
|
|
466
|
+
then: {
|
|
467
|
+
parts: [
|
|
468
|
+
{ type: 'stream-start', warnings: [] },
|
|
469
|
+
{ type: 'text-start', id: 'undo-file-followup' },
|
|
470
|
+
{ type: 'text-delta', id: 'undo-file-followup', delta: 'undo file created' },
|
|
471
|
+
{ type: 'text-end', id: 'undo-file-followup' },
|
|
472
|
+
{
|
|
473
|
+
type: 'finish',
|
|
474
|
+
finishReason: 'stop',
|
|
475
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
476
|
+
},
|
|
477
|
+
],
|
|
478
|
+
},
|
|
479
|
+
}
|
|
480
|
+
|
|
392
481
|
// Multi-step tool chain: model emits text + 3 parallel tool calls in one
|
|
393
482
|
// response (finish="tool-calls"). All tools complete, then the follow-up
|
|
394
483
|
// matcher responds with final text (finish="stop"). This creates 2 assistant
|
|
@@ -601,10 +690,13 @@ export function createDeterministicMatchers(): DeterministicMatcher[] {
|
|
|
601
690
|
pluginTimeoutSleepMatcher,
|
|
602
691
|
actionButtonClickFollowupMatcher,
|
|
603
692
|
questionToolMatcher,
|
|
693
|
+
questionSelectQueueMatcher,
|
|
604
694
|
permissionTypingMatcher,
|
|
605
695
|
permissionTypingFollowupMatcher,
|
|
606
696
|
multiToolMatcher,
|
|
607
697
|
multiToolFollowupMatcher,
|
|
698
|
+
undoFileMatcher,
|
|
699
|
+
undoFileFollowupMatcher,
|
|
608
700
|
multiStepChainInitMatcher,
|
|
609
701
|
multiStepChainStep2Matcher,
|
|
610
702
|
multiStepChainStep3Matcher,
|
|
@@ -62,7 +62,7 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
62
62
|
|
|
63
63
|
const th = ctx.discord.thread(thread.id)
|
|
64
64
|
|
|
65
|
-
await th.waitForTypingEvent({ timeout:
|
|
65
|
+
await th.waitForTypingEvent({ timeout: 4_000 })
|
|
66
66
|
|
|
67
67
|
const pending = await waitForPendingPermission({
|
|
68
68
|
threadId: thread.id,
|
|
@@ -181,7 +181,7 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
181
181
|
discord: ctx.discord,
|
|
182
182
|
threadId: thread.id,
|
|
183
183
|
text: 'Permission dismissed - user sent a new message.',
|
|
184
|
-
timeout:
|
|
184
|
+
timeout: 8_000,
|
|
185
185
|
})
|
|
186
186
|
|
|
187
187
|
await waitForBotReplyAfterUserMessage({
|
|
@@ -189,7 +189,7 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
189
189
|
threadId: thread.id,
|
|
190
190
|
userId: TEST_USER_ID,
|
|
191
191
|
userMessageIncludes: 'post-permission-user-message',
|
|
192
|
-
timeout:
|
|
192
|
+
timeout: 8_000,
|
|
193
193
|
})
|
|
194
194
|
|
|
195
195
|
await waitForBotMessageContaining({
|
|
@@ -198,13 +198,13 @@ describe('queue advanced: typing around permissions', () => {
|
|
|
198
198
|
userId: TEST_USER_ID,
|
|
199
199
|
text: 'ok',
|
|
200
200
|
afterUserMessageIncludes: 'post-permission-user-message',
|
|
201
|
-
timeout:
|
|
201
|
+
timeout: 8_000,
|
|
202
202
|
})
|
|
203
203
|
|
|
204
204
|
await waitForFooterMessage({
|
|
205
205
|
discord: ctx.discord,
|
|
206
206
|
threadId: thread.id,
|
|
207
|
-
timeout:
|
|
207
|
+
timeout: 8_000,
|
|
208
208
|
afterMessageIncludes: 'ok',
|
|
209
209
|
afterAuthorId: ctx.discord.botUserId,
|
|
210
210
|
})
|
|
@@ -72,14 +72,11 @@ e2eTest('queue advanced: typing lifecycle', () => {
|
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
const timeline = await th.text({ showTyping: true })
|
|
75
|
-
expect(timeline).
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
⬥ ok
|
|
81
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
82
|
-
`)
|
|
75
|
+
expect(timeline).toContain('Reply with exactly: typing-stop-normal')
|
|
76
|
+
expect(timeline).toContain('⬥ ok')
|
|
77
|
+
expect(timeline).toContain('*project ⋅ main ⋅')
|
|
78
|
+
const typingCount = (timeline.match(/\[bot typing\]/g) || []).length
|
|
79
|
+
expect(typingCount).toBeGreaterThanOrEqual(1)
|
|
83
80
|
expect(replyIndex).toBeGreaterThanOrEqual(0)
|
|
84
81
|
expect(footerIndex).toBeGreaterThan(replyIndex)
|
|
85
82
|
expect(messages[footerIndex]).toBeDefined()
|
|
@@ -175,20 +172,10 @@ e2eTest('queue advanced: typing lifecycle', () => {
|
|
|
175
172
|
})
|
|
176
173
|
|
|
177
174
|
const timeline = await th.text({ showTyping: true })
|
|
178
|
-
expect(timeline).
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
⬥ ok
|
|
183
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
184
|
-
--- from: user (queue-advanced-tester)
|
|
185
|
-
TYPING_REPULSE_MARKER
|
|
186
|
-
[bot typing]
|
|
187
|
-
--- from: assistant (TestBot)
|
|
188
|
-
⬥ repulse-first
|
|
189
|
-
[bot typing]
|
|
190
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
191
|
-
`)
|
|
175
|
+
expect(timeline).toContain('TYPING_REPULSE_MARKER')
|
|
176
|
+
expect(timeline).toContain('⬥ repulse-first')
|
|
177
|
+
const typingCount = (timeline.match(/\[bot typing\]/g) || []).length
|
|
178
|
+
expect(typingCount).toBeGreaterThanOrEqual(2)
|
|
192
179
|
|
|
193
180
|
const followupUserIndex = messages.findIndex((message) => {
|
|
194
181
|
return message.author.id === TEST_USER_ID
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// E2e test: queued message must drain after the user answers a pending question
|
|
2
|
+
// via the Discord dropdown select menu. Reproduces a bug where answering via
|
|
3
|
+
// select (not text) leaves queued messages stuck because the session continues
|
|
4
|
+
// processing after the answer and may enter another blocking state.
|
|
5
|
+
|
|
6
|
+
import { describe, test, expect } from 'vitest'
|
|
7
|
+
import {
|
|
8
|
+
setupQueueAdvancedSuite,
|
|
9
|
+
TEST_USER_ID,
|
|
10
|
+
} from './queue-advanced-e2e-setup.js'
|
|
11
|
+
import {
|
|
12
|
+
waitForBotMessageContaining,
|
|
13
|
+
waitForFooterMessage,
|
|
14
|
+
} from './test-utils.js'
|
|
15
|
+
import { pendingQuestionContexts } from './commands/ask-question.js'
|
|
16
|
+
|
|
17
|
+
const TEXT_CHANNEL_ID = '200000000000001030'
|
|
18
|
+
|
|
19
|
+
async function waitForPendingQuestion({
|
|
20
|
+
threadId,
|
|
21
|
+
timeoutMs,
|
|
22
|
+
}: {
|
|
23
|
+
threadId: string
|
|
24
|
+
timeoutMs: number
|
|
25
|
+
}): Promise<{ contextHash: string }> {
|
|
26
|
+
const start = Date.now()
|
|
27
|
+
while (Date.now() - start < timeoutMs) {
|
|
28
|
+
const entry = [...pendingQuestionContexts.entries()].find(([, context]) => {
|
|
29
|
+
return context.thread.id === threadId
|
|
30
|
+
})
|
|
31
|
+
if (entry) {
|
|
32
|
+
return { contextHash: entry[0] }
|
|
33
|
+
}
|
|
34
|
+
await new Promise<void>((resolve) => {
|
|
35
|
+
setTimeout(resolve, 100)
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
throw new Error('Timed out waiting for pending question context')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('queue drain after question select answer', () => {
|
|
42
|
+
const ctx = setupQueueAdvancedSuite({
|
|
43
|
+
channelId: TEXT_CHANNEL_ID,
|
|
44
|
+
channelName: 'qa-question-select-drain',
|
|
45
|
+
dirName: 'qa-question-select-drain',
|
|
46
|
+
username: 'question-select-tester',
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test(
|
|
50
|
+
'queued message drains after answering question via dropdown select',
|
|
51
|
+
async () => {
|
|
52
|
+
// 1. Send a message that triggers the question tool
|
|
53
|
+
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
54
|
+
content: 'QUESTION_SELECT_QUEUE_MARKER',
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
58
|
+
timeout: 4_000,
|
|
59
|
+
predicate: (t) => {
|
|
60
|
+
return t.name === 'QUESTION_SELECT_QUEUE_MARKER'
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const th = ctx.discord.thread(thread.id)
|
|
65
|
+
|
|
66
|
+
// 2. Wait for the question dropdown to appear
|
|
67
|
+
const pending = await waitForPendingQuestion({
|
|
68
|
+
threadId: thread.id,
|
|
69
|
+
timeoutMs: 4_000,
|
|
70
|
+
})
|
|
71
|
+
expect(pending.contextHash).toBeTruthy()
|
|
72
|
+
|
|
73
|
+
// Verify dropdown message appeared
|
|
74
|
+
const questionMessages = await waitForBotMessageContaining({
|
|
75
|
+
discord: ctx.discord,
|
|
76
|
+
threadId: thread.id,
|
|
77
|
+
text: 'How to proceed?',
|
|
78
|
+
timeout: 4_000,
|
|
79
|
+
})
|
|
80
|
+
const questionMsg = questionMessages.find((m) => {
|
|
81
|
+
return m.content.includes('How to proceed?')
|
|
82
|
+
})!
|
|
83
|
+
expect(questionMsg).toBeTruthy()
|
|
84
|
+
|
|
85
|
+
// 3. Queue a message while question is pending
|
|
86
|
+
const { id: queueInteractionId } = await th.user(TEST_USER_ID)
|
|
87
|
+
.runSlashCommand({
|
|
88
|
+
name: 'queue',
|
|
89
|
+
options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-question-drain' }],
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const queueAck = await th.waitForInteractionAck({
|
|
93
|
+
interactionId: queueInteractionId,
|
|
94
|
+
timeout: 4_000,
|
|
95
|
+
})
|
|
96
|
+
if (!queueAck.messageId) {
|
|
97
|
+
throw new Error('Expected /queue response message id')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 4. Answer the question via dropdown select (pick first option "Alpha")
|
|
101
|
+
const interaction = await th.user(TEST_USER_ID).selectMenu({
|
|
102
|
+
messageId: questionMsg.id,
|
|
103
|
+
customId: `ask_question:${pending.contextHash}:0`,
|
|
104
|
+
values: ['0'],
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
await th.waitForInteractionAck({
|
|
108
|
+
interactionId: interaction.id,
|
|
109
|
+
timeout: 4_000,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// 5. Queued message should be handed off to OpenCode's own prompt queue
|
|
113
|
+
// after the question reply, so the dispatch indicator appears without
|
|
114
|
+
// waiting for a later natural idle.
|
|
115
|
+
await waitForBotMessageContaining({
|
|
116
|
+
discord: ctx.discord,
|
|
117
|
+
threadId: thread.id,
|
|
118
|
+
text: '» **question-select-tester:** Reply with exactly: post-question-drain',
|
|
119
|
+
timeout: 4_000,
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// 6. Wait for footer from the drained queued message
|
|
123
|
+
await waitForFooterMessage({
|
|
124
|
+
discord: ctx.discord,
|
|
125
|
+
threadId: thread.id,
|
|
126
|
+
timeout: 4_000,
|
|
127
|
+
afterMessageIncludes: '» **question-select-tester:**',
|
|
128
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const timeline = await th.text({ showInteractions: true })
|
|
132
|
+
expect(timeline).toMatchInlineSnapshot(`
|
|
133
|
+
"--- from: user (question-select-tester)
|
|
134
|
+
QUESTION_SELECT_QUEUE_MARKER
|
|
135
|
+
--- from: assistant (TestBot)
|
|
136
|
+
**Select action**
|
|
137
|
+
How to proceed?
|
|
138
|
+
✓ _Alpha_
|
|
139
|
+
[user interaction]
|
|
140
|
+
Queued message (position 1)
|
|
141
|
+
[user selects dropdown: 0]
|
|
142
|
+
» **question-select-tester:** Reply with exactly: post-question-drain
|
|
143
|
+
⬥ ok
|
|
144
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
145
|
+
`)
|
|
146
|
+
},
|
|
147
|
+
20_000,
|
|
148
|
+
)
|
|
149
|
+
})
|
package/src/schema.sql
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
getLatestAssistantMessageIdForLatestUserTurn,
|
|
17
17
|
getLatestRunInfo,
|
|
18
18
|
hasAssistantMessageCompletedBefore,
|
|
19
|
+
doesLatestUserTurnHaveNaturalCompletion,
|
|
19
20
|
isAssistantMessageInLatestUserTurn,
|
|
20
21
|
isAssistantMessageNaturalCompletion,
|
|
21
22
|
isSessionBusy,
|
|
@@ -240,7 +241,11 @@ describe('session-concurrent-messages-serialized', () => {
|
|
|
240
241
|
sessionId,
|
|
241
242
|
})
|
|
242
243
|
|
|
243
|
-
test('fixture
|
|
244
|
+
test('fixture latest turn is still incomplete even though an older turn completed', () => {
|
|
245
|
+
expect(doesLatestUserTurnHaveNaturalCompletion({
|
|
246
|
+
events,
|
|
247
|
+
sessionId,
|
|
248
|
+
})).toBe(false)
|
|
244
249
|
if (!latestAssistantMessageId) {
|
|
245
250
|
throw new Error('Expected latest assistant message')
|
|
246
251
|
}
|
|
@@ -249,7 +254,7 @@ describe('session-concurrent-messages-serialized', () => {
|
|
|
249
254
|
sessionId,
|
|
250
255
|
messageId: latestAssistantMessageId,
|
|
251
256
|
})
|
|
252
|
-
expect(
|
|
257
|
+
expect(message.id).toBe(latestAssistantMessageId)
|
|
253
258
|
})
|
|
254
259
|
})
|
|
255
260
|
|
|
@@ -164,6 +164,7 @@ export function getLatestUserMessage({
|
|
|
164
164
|
upToIndex?: number
|
|
165
165
|
}): UserMessage | undefined {
|
|
166
166
|
const end = upToIndex ?? events.length - 1
|
|
167
|
+
let latestUserMessage: UserMessage | undefined
|
|
167
168
|
for (let i = end; i >= 0; i--) {
|
|
168
169
|
const entry = events[i]
|
|
169
170
|
if (!entry) {
|
|
@@ -177,9 +178,15 @@ export function getLatestUserMessage({
|
|
|
177
178
|
if (info.sessionID !== sessionId || info.role !== 'user') {
|
|
178
179
|
continue
|
|
179
180
|
}
|
|
180
|
-
|
|
181
|
+
if (!latestUserMessage) {
|
|
182
|
+
latestUserMessage = info
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
if (info.time.created > latestUserMessage.time.created) {
|
|
186
|
+
latestUserMessage = info
|
|
187
|
+
}
|
|
181
188
|
}
|
|
182
|
-
return
|
|
189
|
+
return latestUserMessage
|
|
183
190
|
}
|
|
184
191
|
|
|
185
192
|
export function getCurrentTurnStartTime({
|
|
@@ -314,6 +321,9 @@ export function getLatestAssistantMessageIdForLatestUserTurn({
|
|
|
314
321
|
return undefined
|
|
315
322
|
}
|
|
316
323
|
const end = upToIndex ?? events.length - 1
|
|
324
|
+
let latestAssistantMessage:
|
|
325
|
+
| Extract<OpenCodeMessage, { role: 'assistant' }>
|
|
326
|
+
| undefined
|
|
317
327
|
for (let i = end; i >= 0; i--) {
|
|
318
328
|
const entry = events[i]
|
|
319
329
|
if (!entry) {
|
|
@@ -327,11 +337,99 @@ export function getLatestAssistantMessageIdForLatestUserTurn({
|
|
|
327
337
|
if (info.sessionID !== sessionId || info.role !== 'assistant') {
|
|
328
338
|
continue
|
|
329
339
|
}
|
|
330
|
-
if (info.parentID
|
|
331
|
-
|
|
340
|
+
if (info.parentID !== latestUserMessage.id) {
|
|
341
|
+
continue
|
|
342
|
+
}
|
|
343
|
+
if (!latestAssistantMessage) {
|
|
344
|
+
latestAssistantMessage = info
|
|
345
|
+
continue
|
|
346
|
+
}
|
|
347
|
+
if (info.time.created > latestAssistantMessage.time.created) {
|
|
348
|
+
latestAssistantMessage = info
|
|
332
349
|
}
|
|
333
350
|
}
|
|
334
|
-
return
|
|
351
|
+
return latestAssistantMessage?.id
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
type EventBufferedAssistantMessage = AssistantMessage & {
|
|
355
|
+
partsSummary?: Array<{ id: string; type: string }>
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function hasRenderablePartSummary(message: EventBufferedAssistantMessage): boolean {
|
|
359
|
+
if (!('partsSummary' in message) || !Array.isArray(message.partsSummary)) {
|
|
360
|
+
return false
|
|
361
|
+
}
|
|
362
|
+
return message.partsSummary.some((part) => {
|
|
363
|
+
return part.type === 'text' || part.type === 'tool'
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function hasAssistantPartEvidence({
|
|
368
|
+
events,
|
|
369
|
+
sessionId,
|
|
370
|
+
messageId,
|
|
371
|
+
upToIndex,
|
|
372
|
+
}: {
|
|
373
|
+
events: EventBufferEntry[]
|
|
374
|
+
sessionId: string
|
|
375
|
+
messageId: string
|
|
376
|
+
upToIndex?: number
|
|
377
|
+
}): boolean {
|
|
378
|
+
const end = upToIndex ?? events.length - 1
|
|
379
|
+
for (let i = end; i >= 0; i--) {
|
|
380
|
+
const entry = events[i]
|
|
381
|
+
if (!entry) {
|
|
382
|
+
continue
|
|
383
|
+
}
|
|
384
|
+
const event = entry.event
|
|
385
|
+
if (event.type === 'message.updated') {
|
|
386
|
+
const info = event.properties.info as EventBufferedAssistantMessage
|
|
387
|
+
if (info.sessionID !== sessionId || info.role !== 'assistant' || info.id !== messageId) {
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
if (hasRenderablePartSummary(info)) {
|
|
391
|
+
return true
|
|
392
|
+
}
|
|
393
|
+
continue
|
|
394
|
+
}
|
|
395
|
+
if (event.type !== 'message.part.updated') {
|
|
396
|
+
continue
|
|
397
|
+
}
|
|
398
|
+
const { part } = event.properties
|
|
399
|
+
if (part.messageID !== messageId) {
|
|
400
|
+
continue
|
|
401
|
+
}
|
|
402
|
+
if (part.type === 'text' || part.type === 'tool') {
|
|
403
|
+
return true
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return false
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function hasAssistantStepFinished({
|
|
410
|
+
events,
|
|
411
|
+
messageId,
|
|
412
|
+
upToIndex,
|
|
413
|
+
}: {
|
|
414
|
+
events: EventBufferEntry[]
|
|
415
|
+
messageId: string
|
|
416
|
+
upToIndex?: number
|
|
417
|
+
}): boolean {
|
|
418
|
+
const end = upToIndex ?? events.length - 1
|
|
419
|
+
for (let i = end; i >= 0; i--) {
|
|
420
|
+
const entry = events[i]
|
|
421
|
+
if (!entry || entry.event.type !== 'message.part.updated') {
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
const { part } = entry.event.properties
|
|
425
|
+
if (part.messageID !== messageId) {
|
|
426
|
+
continue
|
|
427
|
+
}
|
|
428
|
+
if (part.type === 'step-finish') {
|
|
429
|
+
return true
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return false
|
|
335
433
|
}
|
|
336
434
|
|
|
337
435
|
export function doesLatestUserTurnHaveNaturalCompletion({
|
|
@@ -353,6 +451,7 @@ export function doesLatestUserTurnHaveNaturalCompletion({
|
|
|
353
451
|
}
|
|
354
452
|
|
|
355
453
|
const end = upToIndex ?? events.length - 1
|
|
454
|
+
let latestAssistantMessage: EventBufferedAssistantMessage | undefined
|
|
356
455
|
for (let i = end; i >= 0; i--) {
|
|
357
456
|
const entry = events[i]
|
|
358
457
|
if (!entry) {
|
|
@@ -369,10 +468,32 @@ export function doesLatestUserTurnHaveNaturalCompletion({
|
|
|
369
468
|
if (info.id !== latestAssistantMessageId) {
|
|
370
469
|
continue
|
|
371
470
|
}
|
|
372
|
-
|
|
471
|
+
latestAssistantMessage = info as EventBufferedAssistantMessage
|
|
472
|
+
if (isAssistantMessageNaturalCompletion({ message: info })) {
|
|
473
|
+
return true
|
|
474
|
+
}
|
|
475
|
+
break
|
|
373
476
|
}
|
|
374
477
|
|
|
375
|
-
|
|
478
|
+
if (!latestAssistantMessage) {
|
|
479
|
+
return false
|
|
480
|
+
}
|
|
481
|
+
if (latestAssistantMessage.error) {
|
|
482
|
+
return false
|
|
483
|
+
}
|
|
484
|
+
if (latestAssistantMessage.finish === 'tool-calls') {
|
|
485
|
+
return false
|
|
486
|
+
}
|
|
487
|
+
return hasAssistantStepFinished({
|
|
488
|
+
events,
|
|
489
|
+
messageId: latestAssistantMessageId,
|
|
490
|
+
upToIndex,
|
|
491
|
+
}) && hasAssistantPartEvidence({
|
|
492
|
+
events,
|
|
493
|
+
sessionId,
|
|
494
|
+
messageId: latestAssistantMessageId,
|
|
495
|
+
upToIndex,
|
|
496
|
+
})
|
|
376
497
|
}
|
|
377
498
|
|
|
378
499
|
export function isAssistantMessageInLatestUserTurn({
|
|
@@ -42,6 +42,11 @@ export type QueuedMessage = {
|
|
|
42
42
|
// Raw permission rule strings ("tool:action" or "tool:pattern:action").
|
|
43
43
|
// Parsed and merged into session permissions on creation.
|
|
44
44
|
permissions?: string[]
|
|
45
|
+
// Discord message ID and thread ID of the source message. Embedded in
|
|
46
|
+
// <discord-user> synthetic context so the external sync loop can detect
|
|
47
|
+
// messages that originated from Discord and skip re-mirroring them.
|
|
48
|
+
sourceMessageId?: string
|
|
49
|
+
sourceThreadId?: string
|
|
45
50
|
// Tracking fields for scheduled tasks. Stored in the DB via
|
|
46
51
|
// setSessionStartSource() after the session is created, so the session
|
|
47
52
|
// list can show which sessions were started by scheduled tasks.
|