kimaki 0.28.0 → 0.30.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 +33 -33
- package/dist/anthropic-account-identity.js +1 -1
- package/dist/anthropic-auth-plugin.js +5 -5
- package/dist/anthropic-auth-plugin.test.js +30 -2
- package/dist/anthropic-auth-state.js +1 -1
- package/dist/bash-tool-schema-plugin.js +18 -0
- package/dist/bash-tool-schema-plugin.test.js +20 -1
- package/dist/bin.js +7 -3
- package/dist/bot-lifecycle.test.js +206 -0
- package/dist/cache-drift-plugin.js +25 -18
- package/dist/cache-rewrite.js +39 -0
- package/dist/cache-rewrite.test.js +41 -0
- package/dist/channel-management.js +113 -51
- package/dist/cli-commands/misc.js +4 -1
- package/dist/cli-commands/project.js +7 -4
- package/dist/cli-commands/session.js +118 -36
- package/dist/cli-commands/thread.js +63 -0
- package/dist/cli-parsing.test.js +3 -3
- package/dist/cli-runner.js +11 -1
- package/dist/cli-send-thread.e2e.test.js +2 -2
- package/dist/cli.js +6 -4
- package/dist/commands/ask-question.js +72 -23
- package/dist/commands/queue.js +84 -36
- package/dist/commands/queue.test.js +15 -0
- package/dist/commands/restart-opencode-server.js +7 -1
- package/dist/commands/session-id.js +5 -5
- package/dist/database.js +149 -6
- package/dist/db.js +34 -0
- package/dist/db.test.js +199 -1
- package/dist/default-channel-provisioning.test.js +2 -1
- package/dist/discord-bot.js +220 -146
- package/dist/discord-command-registration.js +1 -1
- package/dist/external-directory-permissions.e2e.test.js +3 -3
- package/dist/external-directory-project-deny.e2e.test.js +1 -1
- package/dist/forum-sync/watchers.js +15 -0
- package/dist/gateway-proxy.e2e.test.js +9 -9
- package/dist/guild-categories.test.js +225 -0
- package/dist/hrana-server.js +68 -14
- package/dist/interaction-handler.js +5 -1
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/kimaki-opencode-plugin.test.js +28 -0
- package/dist/list-channel-threads.js +163 -0
- package/dist/list-channel-threads.test.js +80 -0
- package/dist/markdown.js +164 -102
- package/dist/markdown.test.js +88 -20
- package/dist/message-formatting.js +29 -18
- package/dist/message-formatting.test.js +111 -3
- package/dist/opencode.js +27 -12
- package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
- package/dist/question-abort-resume.e2e.test.js +100 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +1 -1
- package/dist/question-text-before-dropdown.e2e.test.js +1 -1
- package/dist/queue-advanced-abort.e2e.test.js +3 -3
- package/dist/queue-advanced-action-buttons.e2e.test.js +5 -5
- package/dist/queue-advanced-e2e-setup.js +63 -0
- package/dist/queue-advanced-footer.e2e.test.js +18 -18
- package/dist/queue-advanced-model-switch.e2e.test.js +3 -4
- package/dist/queue-advanced-permissions-typing.e2e.test.js +2 -2
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +3 -3
- package/dist/queue-delete-message.e2e.test.js +4 -4
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +3 -3
- package/dist/queue-interrupt-drain.e2e.test.js +10 -10
- package/dist/queue-question-select-drain.e2e.test.js +5 -5
- package/dist/runtime-lifecycle.e2e.test.js +95 -11
- package/dist/schema.js +21 -0
- package/dist/session-handler/event-stream-state.js +240 -6
- package/dist/session-handler/event-stream-state.test.js +1116 -130
- package/dist/session-handler/thread-runtime-state.js +4 -0
- package/dist/session-handler/thread-session-runtime.js +499 -89
- package/dist/sleep.e2e.test.js +7 -7
- package/dist/store.js +1 -1
- package/dist/system-message.js +64 -23
- package/dist/system-message.test.js +69 -23
- package/dist/task-id-plugin.js +16 -0
- package/dist/task-runner.js +7 -0
- package/dist/task-schedule.test.js +1 -1
- package/dist/test-utils.js +5 -3
- package/dist/thread-message-queue.e2e.test.js +30 -31
- package/dist/undo-redo.e2e.test.js +3 -3
- package/dist/voice-message.e2e.test.js +64 -26
- package/dist/voice-routing-input.e2e.test.js +4 -4
- package/dist/voice-routing-question.e2e.test.js +4 -4
- package/dist/voice.js +107 -21
- package/dist/voice.test.js +52 -1
- package/dist/wait-session.js +24 -2
- package/dist/worktree-lifecycle.e2e.test.js +8 -8
- package/package.json +7 -7
- package/skills/egaki/SKILL.md +14 -0
- package/skills/holocron/SKILL.md +125 -42
- package/skills/opensrc/SKILL.md +20 -3
- package/skills/spiceflow/SKILL.md +53 -172
- package/skills/strada/SKILL.md +61 -232
- package/src/agent-model.e2e.test.ts +33 -33
- package/src/anthropic-account-identity.ts +1 -1
- package/src/anthropic-auth-plugin.test.ts +37 -2
- package/src/anthropic-auth-plugin.ts +9 -7
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +29 -0
- package/src/bash-tool-schema-plugin.ts +31 -0
- package/src/bin.ts +7 -3
- package/src/bot-lifecycle.test.ts +252 -0
- package/src/cache-drift-plugin.ts +34 -24
- package/src/cache-rewrite.test.ts +49 -0
- package/src/cache-rewrite.ts +96 -0
- package/src/channel-management.ts +144 -65
- package/src/cli-commands/misc.ts +4 -1
- package/src/cli-commands/project.ts +9 -4
- package/src/cli-commands/session.ts +167 -38
- package/src/cli-commands/thread.ts +86 -0
- package/src/cli-parsing.test.ts +3 -3
- package/src/cli-runner.ts +11 -0
- package/src/cli-send-thread.e2e.test.ts +2 -2
- package/src/cli.ts +8 -6
- package/src/commands/ask-question.ts +88 -25
- package/src/commands/queue.test.ts +19 -0
- package/src/commands/queue.ts +111 -45
- package/src/commands/restart-opencode-server.ts +12 -1
- package/src/commands/session-id.ts +5 -5
- package/src/database.ts +219 -6
- package/src/db.test.ts +236 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +2 -1
- package/src/discord-bot.ts +272 -185
- package/src/discord-command-registration.ts +3 -1
- package/src/external-directory-permissions.e2e.test.ts +3 -3
- package/src/external-directory-project-deny.e2e.test.ts +1 -1
- package/src/forum-sync/watchers.ts +20 -0
- package/src/gateway-proxy.e2e.test.ts +9 -9
- package/src/guild-categories.test.ts +260 -0
- package/src/hrana-server.ts +96 -16
- package/src/interaction-handler.ts +7 -0
- package/src/kimaki-opencode-plugin.test.ts +34 -0
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/list-channel-threads.test.ts +90 -0
- package/src/list-channel-threads.ts +252 -0
- package/src/markdown.test.ts +108 -22
- package/src/markdown.ts +216 -103
- package/src/message-formatting.test.ts +129 -2
- package/src/message-formatting.ts +28 -14
- package/src/opencode.ts +28 -13
- package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
- package/src/question-abort-resume.e2e.test.ts +121 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +1 -1
- package/src/question-text-before-dropdown.e2e.test.ts +1 -1
- package/src/queue-advanced-abort.e2e.test.ts +3 -3
- package/src/queue-advanced-action-buttons.e2e.test.ts +5 -5
- package/src/queue-advanced-e2e-setup.ts +65 -0
- package/src/queue-advanced-footer.e2e.test.ts +18 -18
- package/src/queue-advanced-model-switch.e2e.test.ts +3 -4
- package/src/queue-advanced-permissions-typing.e2e.test.ts +2 -2
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +3 -3
- package/src/queue-delete-message.e2e.test.ts +4 -4
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +3 -3
- package/src/queue-interrupt-drain.e2e.test.ts +10 -10
- package/src/queue-question-select-drain.e2e.test.ts +5 -5
- package/src/runtime-lifecycle.e2e.test.ts +104 -10
- package/src/schema.sql +16 -0
- package/src/schema.ts +23 -0
- package/src/session-handler/event-stream-state.test.ts +1184 -133
- package/src/session-handler/event-stream-state.ts +322 -8
- package/src/session-handler/thread-runtime-state.ts +5 -0
- package/src/session-handler/thread-session-runtime.ts +597 -95
- package/src/sleep.e2e.test.ts +7 -7
- package/src/store.ts +2 -2
- package/src/system-message.test.ts +70 -23
- package/src/system-message.ts +67 -22
- package/src/task-id-plugin.ts +17 -0
- package/src/task-runner.ts +8 -0
- package/src/task-schedule.test.ts +1 -1
- package/src/test-utils.ts +5 -3
- package/src/thread-message-queue.e2e.test.ts +30 -31
- package/src/undo-redo.e2e.test.ts +3 -3
- package/src/voice-message.e2e.test.ts +70 -26
- package/src/voice-routing-input.e2e.test.ts +4 -4
- package/src/voice-routing-question.e2e.test.ts +4 -4
- package/src/voice.test.ts +56 -0
- package/src/voice.ts +135 -25
- package/src/wait-session.ts +25 -2
- package/src/worktree-lifecycle.e2e.test.ts +8 -8
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import crypto from 'node:crypto'
|
|
10
10
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
11
|
-
import { ChannelType, type ThreadChannel } from 'discord.js'
|
|
11
|
+
import { ChannelType, type Client, type ThreadChannel } from 'discord.js'
|
|
12
12
|
import type {
|
|
13
13
|
Event as OpenCodeEvent,
|
|
14
14
|
Part,
|
|
@@ -45,14 +45,17 @@ import {
|
|
|
45
45
|
raceDiscordRename,
|
|
46
46
|
DISCORD_THREAD_RENAME_TIMEOUT_MS,
|
|
47
47
|
resolveThreadFooterMentionUserId,
|
|
48
|
+
resolveWorkingDirectory,
|
|
48
49
|
} from '../discord-utils.js'
|
|
49
50
|
import type { DiscordFileAttachment, SessionPartKind } from '../message-formatting.js'
|
|
50
51
|
import {
|
|
51
52
|
asDiscordQuote,
|
|
53
|
+
asSystemLine,
|
|
52
54
|
formatPart,
|
|
53
55
|
formatTaskToolTitle,
|
|
54
56
|
planAssistantTurnFlush,
|
|
55
57
|
QUEUE_PREFIX,
|
|
58
|
+
sessionPartContent,
|
|
56
59
|
sessionPartKind,
|
|
57
60
|
shouldLeadWithBlankLine,
|
|
58
61
|
shouldQuoteIntermediateTextPart,
|
|
@@ -83,6 +86,11 @@ import {
|
|
|
83
86
|
appendSessionEventsSinceLastTimestamp,
|
|
84
87
|
getSessionEventSnapshot,
|
|
85
88
|
cancelSessionSleepForThread,
|
|
89
|
+
insertThreadQueueItem,
|
|
90
|
+
listAllThreadQueueItems,
|
|
91
|
+
deleteThreadQueueItem,
|
|
92
|
+
deleteThreadQueueItems,
|
|
93
|
+
updateThreadQueueItemPayload,
|
|
86
94
|
} from '../database.js'
|
|
87
95
|
import * as orm from 'drizzle-orm'
|
|
88
96
|
import * as schema from '../schema.js'
|
|
@@ -127,6 +135,7 @@ import {
|
|
|
127
135
|
type ScheduledTaskSystemContext,
|
|
128
136
|
} from '../system-message.js'
|
|
129
137
|
import { getDataDir } from '../config.js'
|
|
138
|
+
import { countSystemPromptDiffLines } from '../cache-rewrite.js'
|
|
130
139
|
import { store } from '../store.js'
|
|
131
140
|
import {
|
|
132
141
|
trackEvent,
|
|
@@ -150,6 +159,8 @@ import {
|
|
|
150
159
|
getCurrentTurnStartTime,
|
|
151
160
|
isSessionBusy,
|
|
152
161
|
getLatestRunInfo,
|
|
162
|
+
getPromptCacheClear,
|
|
163
|
+
formatPromptCacheClearMessage,
|
|
153
164
|
getIdleTokenUsageDelta,
|
|
154
165
|
getDerivedSubtaskIndex,
|
|
155
166
|
getDerivedSubtaskAgentType,
|
|
@@ -161,6 +172,8 @@ import {
|
|
|
161
172
|
isAssistantMessageInLatestUserTurn,
|
|
162
173
|
isAssistantMessageNaturalCompletion,
|
|
163
174
|
shouldBufferSessionEvent,
|
|
175
|
+
shouldRetainSessionEvent,
|
|
176
|
+
trimEventBuffer,
|
|
164
177
|
type EventBufferEvent,
|
|
165
178
|
type EventBufferEntry,
|
|
166
179
|
} from './event-stream-state.js'
|
|
@@ -320,11 +333,92 @@ export function getOrCreateRuntime(
|
|
|
320
333
|
return runtime
|
|
321
334
|
}
|
|
322
335
|
|
|
323
|
-
|
|
336
|
+
function groupQueueRowsByThread(
|
|
337
|
+
rows: Array<{ thread_id: string; queue_id: string; payload_json: string }>,
|
|
338
|
+
): Map<string, QueuedMessage[]> {
|
|
339
|
+
const byThread = new Map<string, QueuedMessage[]>()
|
|
340
|
+
for (const row of rows) {
|
|
341
|
+
const parsed = parseQueuedMessagePayload({
|
|
342
|
+
queueId: row.queue_id,
|
|
343
|
+
payloadJson: row.payload_json,
|
|
344
|
+
})
|
|
345
|
+
if (parsed instanceof Error) {
|
|
346
|
+
logger.warn(
|
|
347
|
+
`[QUEUE] Skipping invalid queue row ${row.queue_id} in thread ${row.thread_id}: ${parsed.message}`,
|
|
348
|
+
)
|
|
349
|
+
continue
|
|
350
|
+
}
|
|
351
|
+
const items = byThread.get(row.thread_id) ?? []
|
|
352
|
+
items.push(parsed)
|
|
353
|
+
byThread.set(row.thread_id, items)
|
|
354
|
+
}
|
|
355
|
+
return byThread
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function restorePersistedLocalQueues({
|
|
359
|
+
discordClient,
|
|
360
|
+
appId,
|
|
361
|
+
}: {
|
|
362
|
+
discordClient: Client
|
|
363
|
+
appId?: string
|
|
364
|
+
}): Promise<void> {
|
|
365
|
+
const rows = await listAllThreadQueueItems()
|
|
366
|
+
if (rows.length === 0) {
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const byThread = groupQueueRowsByThread(rows)
|
|
371
|
+
for (const [threadId, items] of byThread) {
|
|
372
|
+
if (items.length === 0) {
|
|
373
|
+
continue
|
|
374
|
+
}
|
|
375
|
+
let runtime = runtimes.get(threadId)
|
|
376
|
+
if (!runtime) {
|
|
377
|
+
const fetched = await discordClient.channels.fetch(threadId).catch((error) => {
|
|
378
|
+
logger.warn(
|
|
379
|
+
`[QUEUE] Failed to fetch thread ${threadId} for restored queue: ${error instanceof Error ? error.message : String(error)}`,
|
|
380
|
+
)
|
|
381
|
+
return null
|
|
382
|
+
})
|
|
383
|
+
if (!fetched?.isThread()) {
|
|
384
|
+
logger.warn(`[QUEUE] Skipping restored queue for missing thread ${threadId}`)
|
|
385
|
+
continue
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const resolved = await resolveWorkingDirectory({ channel: fetched })
|
|
389
|
+
if (!resolved) {
|
|
390
|
+
logger.warn(`[QUEUE] Skipping restored queue for thread ${threadId}: no project directory`)
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const sessionId = await getThreadSession(threadId)
|
|
395
|
+
runtime = getOrCreateRuntime({
|
|
396
|
+
threadId,
|
|
397
|
+
thread: fetched,
|
|
398
|
+
projectDirectory: resolved.projectDirectory,
|
|
399
|
+
sdkDirectory: resolved.workingDirectory,
|
|
400
|
+
channelId: fetched.parentId || fetched.id,
|
|
401
|
+
appId,
|
|
402
|
+
sessionId,
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
await runtime.dispatchAction(() => {
|
|
406
|
+
return runtime.mergeRestoredQueueAndDrain(items)
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function disposeRuntime(
|
|
412
|
+
threadId: string,
|
|
413
|
+
{ abortActiveRun = false }: { abortActiveRun?: boolean } = {},
|
|
414
|
+
): void {
|
|
324
415
|
const runtime = runtimes.get(threadId)
|
|
325
416
|
if (!runtime) {
|
|
326
417
|
return
|
|
327
418
|
}
|
|
419
|
+
if (abortActiveRun) {
|
|
420
|
+
runtime.abortDeletedDiscordResource()
|
|
421
|
+
}
|
|
328
422
|
runtime.dispose()
|
|
329
423
|
runtimes.delete(threadId)
|
|
330
424
|
threadState.removeThread(threadId) // remove from global store
|
|
@@ -355,6 +449,12 @@ export function disposeRuntimesForDirectory({
|
|
|
355
449
|
return count
|
|
356
450
|
}
|
|
357
451
|
|
|
452
|
+
export function getRuntimeThreadIdsForChannel(channelId: string): string[] {
|
|
453
|
+
return Array.from(runtimes.entries())
|
|
454
|
+
.filter(([, runtime]) => runtime.channelId === channelId)
|
|
455
|
+
.map(([threadId]) => threadId)
|
|
456
|
+
}
|
|
457
|
+
|
|
358
458
|
/** Returns number of active runtimes (useful for diagnostics). */
|
|
359
459
|
export function getRuntimeCount(): number {
|
|
360
460
|
return runtimes.size
|
|
@@ -722,6 +822,36 @@ function trackTurnStarted({
|
|
|
722
822
|
})
|
|
723
823
|
}
|
|
724
824
|
|
|
825
|
+
function parseQueuedMessagePayload({
|
|
826
|
+
queueId,
|
|
827
|
+
payloadJson,
|
|
828
|
+
}: {
|
|
829
|
+
queueId: string
|
|
830
|
+
payloadJson: string
|
|
831
|
+
}): QueuedMessage | Error {
|
|
832
|
+
return errore.try(
|
|
833
|
+
() => {
|
|
834
|
+
const parsed = JSON.parse(payloadJson) as QueuedMessage
|
|
835
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
836
|
+
return new Error('Queued message payload is not an object')
|
|
837
|
+
}
|
|
838
|
+
if (typeof parsed.prompt !== 'string') {
|
|
839
|
+
return new Error('Queued message payload is missing prompt')
|
|
840
|
+
}
|
|
841
|
+
if (typeof parsed.userId !== 'string') {
|
|
842
|
+
return new Error('Queued message payload is missing userId')
|
|
843
|
+
}
|
|
844
|
+
if (typeof parsed.username !== 'string') {
|
|
845
|
+
return new Error('Queued message payload is missing username')
|
|
846
|
+
}
|
|
847
|
+
return { ...parsed, queueId }
|
|
848
|
+
},
|
|
849
|
+
(error) => {
|
|
850
|
+
return new Error('Failed to parse queued message payload', { cause: error })
|
|
851
|
+
},
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
|
|
725
855
|
// Rewrite `{ prompt: "/build foo" }` → `{ prompt: "", command: { name, arguments }, mode: "local-queue" }`
|
|
726
856
|
// when the prompt's leading token matches a registered opencode command.
|
|
727
857
|
// Skip if a command is already set or there's no prompt to inspect.
|
|
@@ -770,6 +900,7 @@ export class ThreadSessionRuntime {
|
|
|
770
900
|
|
|
771
901
|
// Set to true by dispose(). Guards against queued work running after cleanup.
|
|
772
902
|
private disposed = false
|
|
903
|
+
private dispatchingQueueId: string | undefined
|
|
773
904
|
|
|
774
905
|
// Typing indicator scheduler handles.
|
|
775
906
|
// `typingKeepaliveTimeout` is the 7s keepalive loop while a run stays busy.
|
|
@@ -786,6 +917,7 @@ export class ThreadSessionRuntime {
|
|
|
786
917
|
// Notification throttles for retry/context notices.
|
|
787
918
|
private lastDisplayedContextPercentage = 0
|
|
788
919
|
private lastRateLimitDisplayTime = 0
|
|
920
|
+
private userSystemByMessageId = new Map<string, string>()
|
|
789
921
|
|
|
790
922
|
// Last OpenCode session title we applied to Discord. Dedupes session.updated
|
|
791
923
|
// so we only call setName once per distinct title. Not persisted.
|
|
@@ -900,7 +1032,7 @@ export class ThreadSessionRuntime {
|
|
|
900
1032
|
}
|
|
901
1033
|
|
|
902
1034
|
getDerivedPhase(): 'idle' | 'running' {
|
|
903
|
-
return this.
|
|
1035
|
+
return this.isBusy() ? 'running' : 'idle'
|
|
904
1036
|
}
|
|
905
1037
|
|
|
906
1038
|
private getLastRuntimeActivityTimestamp({
|
|
@@ -929,7 +1061,7 @@ export class ThreadSessionRuntime {
|
|
|
929
1061
|
}
|
|
930
1062
|
|
|
931
1063
|
private isIdleCandidateForInactivityCheck(): boolean {
|
|
932
|
-
if (this.
|
|
1064
|
+
if (this.isBusy()) {
|
|
933
1065
|
return false
|
|
934
1066
|
}
|
|
935
1067
|
if ((this.state?.queueItems.length ?? 0) > 0) {
|
|
@@ -1013,7 +1145,18 @@ export class ThreadSessionRuntime {
|
|
|
1013
1145
|
]
|
|
1014
1146
|
})
|
|
1015
1147
|
|
|
1016
|
-
this.eventBuffer =
|
|
1148
|
+
this.eventBuffer = trimEventBuffer({
|
|
1149
|
+
events: hydratedEvents,
|
|
1150
|
+
mainSessionId: sessionId,
|
|
1151
|
+
max: ThreadSessionRuntime.EVENT_BUFFER_MAX,
|
|
1152
|
+
isKnownChildSession: (candidateSessionId) => {
|
|
1153
|
+
return isDerivedChildSession({
|
|
1154
|
+
events: hydratedEvents,
|
|
1155
|
+
mainSessionId: sessionId,
|
|
1156
|
+
candidateSessionId,
|
|
1157
|
+
})
|
|
1158
|
+
},
|
|
1159
|
+
})
|
|
1017
1160
|
const lastHydratedEvent = this.eventBuffer[this.eventBuffer.length - 1]
|
|
1018
1161
|
this.nextEventIndex = lastHydratedEvent
|
|
1019
1162
|
? Number(lastHydratedEvent.eventIndex || 0) + 1
|
|
@@ -1072,7 +1215,8 @@ export class ThreadSessionRuntime {
|
|
|
1072
1215
|
return `phase=${phase},assistant=${latestAssistant},assistantCount=${assistantCount}`
|
|
1073
1216
|
}
|
|
1074
1217
|
|
|
1075
|
-
|
|
1218
|
+
/** Whether the main session currently has an active run (derived from events). */
|
|
1219
|
+
isBusy(): boolean {
|
|
1076
1220
|
const sessionId = this.state?.sessionId
|
|
1077
1221
|
if (!sessionId) {
|
|
1078
1222
|
return false
|
|
@@ -1190,6 +1334,14 @@ export class ThreadSessionRuntime {
|
|
|
1190
1334
|
|
|
1191
1335
|
// ── Lifecycle ────────────────────────────────────────────────
|
|
1192
1336
|
|
|
1337
|
+
abortDeletedDiscordResource(): void {
|
|
1338
|
+
if (this.getDerivedPhase() === 'running') {
|
|
1339
|
+
void this.abortActiveRunInternal({
|
|
1340
|
+
reason: 'discord-resource-deleted',
|
|
1341
|
+
}).apiAbortPromise
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1193
1345
|
dispose(): void {
|
|
1194
1346
|
this.disposed = true
|
|
1195
1347
|
unregisterEventListener(this.threadId)
|
|
@@ -1312,6 +1464,9 @@ export class ThreadSessionRuntime {
|
|
|
1312
1464
|
return [{ id: candidate.id, type: candidate.type }]
|
|
1313
1465
|
})
|
|
1314
1466
|
: []
|
|
1467
|
+
if (info.role === 'user' && typeof info.id === 'string' && typeof info.system === 'string') {
|
|
1468
|
+
this.userSystemByMessageId.set(info.id, info.system)
|
|
1469
|
+
}
|
|
1315
1470
|
delete info.system
|
|
1316
1471
|
delete info.tools
|
|
1317
1472
|
delete info.parts
|
|
@@ -1389,6 +1544,15 @@ export class ThreadSessionRuntime {
|
|
|
1389
1544
|
if (!compactedEvent) {
|
|
1390
1545
|
return
|
|
1391
1546
|
}
|
|
1547
|
+
if (!shouldRetainSessionEvent({
|
|
1548
|
+
event: compactedEvent,
|
|
1549
|
+
mainSessionId: this.state?.sessionId,
|
|
1550
|
+
isKnownChildSession: (candidateSessionId) => {
|
|
1551
|
+
return Boolean(this.getSubtaskInfoForSession(candidateSessionId))
|
|
1552
|
+
},
|
|
1553
|
+
})) {
|
|
1554
|
+
return
|
|
1555
|
+
}
|
|
1392
1556
|
|
|
1393
1557
|
const timestamp = Date.now()
|
|
1394
1558
|
const eventIndex = this.nextEventIndex
|
|
@@ -1398,9 +1562,14 @@ export class ThreadSessionRuntime {
|
|
|
1398
1562
|
timestamp,
|
|
1399
1563
|
eventIndex,
|
|
1400
1564
|
})
|
|
1401
|
-
|
|
1402
|
-
this.eventBuffer
|
|
1403
|
-
|
|
1565
|
+
this.eventBuffer = trimEventBuffer({
|
|
1566
|
+
events: this.eventBuffer,
|
|
1567
|
+
mainSessionId: this.state?.sessionId,
|
|
1568
|
+
max: ThreadSessionRuntime.EVENT_BUFFER_MAX,
|
|
1569
|
+
isKnownChildSession: (candidateSessionId) => {
|
|
1570
|
+
return Boolean(this.getSubtaskInfoForSession(candidateSessionId))
|
|
1571
|
+
},
|
|
1572
|
+
})
|
|
1404
1573
|
this.persistEventBufferDebounced.trigger()
|
|
1405
1574
|
}
|
|
1406
1575
|
|
|
@@ -1513,6 +1682,7 @@ export class ThreadSessionRuntime {
|
|
|
1513
1682
|
// causing tryDrainQueue to drain the local queue while the session is
|
|
1514
1683
|
// actually still busy. This was the root cause of "? queue" messages
|
|
1515
1684
|
// interrupting instead of queuing.
|
|
1685
|
+
// Child task part floods are also dropped at retain time for the same reason.
|
|
1516
1686
|
if (event.type !== 'message.part.delta') {
|
|
1517
1687
|
this.appendEventToBuffer(event)
|
|
1518
1688
|
}
|
|
@@ -1872,6 +2042,9 @@ export class ThreadSessionRuntime {
|
|
|
1872
2042
|
if (part.type === 'text' && !part.time?.end && mode === 'progress') {
|
|
1873
2043
|
return false
|
|
1874
2044
|
}
|
|
2045
|
+
if (part.type === 'text' && part.synthetic === true) {
|
|
2046
|
+
return false
|
|
2047
|
+
}
|
|
1875
2048
|
return true
|
|
1876
2049
|
}
|
|
1877
2050
|
|
|
@@ -1883,7 +2056,50 @@ export class ThreadSessionRuntime {
|
|
|
1883
2056
|
if (messageIds.length > 0) {
|
|
1884
2057
|
return messageIds.flatMap((id) => this.getBufferedParts(id))
|
|
1885
2058
|
}
|
|
1886
|
-
return [
|
|
2059
|
+
return []
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
private async unquoteFinalTextPart(): Promise<void> {
|
|
2063
|
+
const parts = this.getCurrentTurnParts()
|
|
2064
|
+
const finalPart = planAssistantTurnFlush({
|
|
2065
|
+
parts,
|
|
2066
|
+
mode: 'final',
|
|
2067
|
+
}).sendParts.at(-1)
|
|
2068
|
+
if (!finalPart || finalPart.quoteText || finalPart.part.type !== 'text') return
|
|
2069
|
+
const last = finalPart.part
|
|
2070
|
+
const db = await getDb()
|
|
2071
|
+
const row = await db.query.part_messages.findFirst({
|
|
2072
|
+
where: { part_id: last.id },
|
|
2073
|
+
columns: { message_id: true },
|
|
2074
|
+
}).catch((e) => new DiscordOperationError({ operation: 'getPartMessage', cause: e }))
|
|
2075
|
+
if (row instanceof Error) {
|
|
2076
|
+
discordLogger.error(`Failed to find Discord message for ${last.id}:`, row)
|
|
2077
|
+
return
|
|
2078
|
+
}
|
|
2079
|
+
const messageId = row?.message_id
|
|
2080
|
+
if (!messageId) return
|
|
2081
|
+
const message = await this.thread.messages.fetch(messageId)
|
|
2082
|
+
.catch((e) => new DiscordOperationError({ operation: 'fetchMessage', cause: e }))
|
|
2083
|
+
if (message instanceof Error) {
|
|
2084
|
+
discordLogger.error(`Failed to fetch Discord message for ${last.id}:`, message)
|
|
2085
|
+
return
|
|
2086
|
+
}
|
|
2087
|
+
const formatted = formatPart(last)
|
|
2088
|
+
const leadWithBlankLine = message.content.startsWith('\n')
|
|
2089
|
+
const quoted = sessionPartContent({
|
|
2090
|
+
content: asDiscordQuote(formatted),
|
|
2091
|
+
leadWithBlankLine,
|
|
2092
|
+
})
|
|
2093
|
+
if (message.content !== quoted) return
|
|
2094
|
+
const plain = sessionPartContent({
|
|
2095
|
+
content: formatted,
|
|
2096
|
+
leadWithBlankLine,
|
|
2097
|
+
})
|
|
2098
|
+
const edited = await message.edit({ content: plain })
|
|
2099
|
+
.catch((e) => new DiscordOperationError({ operation: 'editMessage', cause: e }))
|
|
2100
|
+
if (edited instanceof Error) {
|
|
2101
|
+
discordLogger.error(`ERROR: Failed to unquote final text ${last.id}:`, edited)
|
|
2102
|
+
}
|
|
1887
2103
|
}
|
|
1888
2104
|
|
|
1889
2105
|
private async flushCurrentTurnParts({
|
|
@@ -1897,15 +2113,16 @@ export class ThreadSessionRuntime {
|
|
|
1897
2113
|
skipPartId?: string
|
|
1898
2114
|
repulseTyping?: boolean
|
|
1899
2115
|
}): Promise<void> {
|
|
1900
|
-
const parts = this.getCurrentTurnParts()
|
|
1901
|
-
return !this.state?.sentPartIds.has(part.id)
|
|
1902
|
-
})
|
|
2116
|
+
const parts = this.getCurrentTurnParts()
|
|
1903
2117
|
const planned = planAssistantTurnFlush({
|
|
1904
2118
|
parts,
|
|
1905
2119
|
mode,
|
|
1906
2120
|
throughPartId,
|
|
1907
2121
|
})
|
|
1908
2122
|
for (const { part, quoteText } of planned.sendParts) {
|
|
2123
|
+
if (this.state?.sentPartIds.has(part.id)) {
|
|
2124
|
+
continue
|
|
2125
|
+
}
|
|
1909
2126
|
if (skipPartId && part.id === skipPartId) {
|
|
1910
2127
|
continue
|
|
1911
2128
|
}
|
|
@@ -2203,6 +2420,15 @@ export class ThreadSessionRuntime {
|
|
|
2203
2420
|
return
|
|
2204
2421
|
}
|
|
2205
2422
|
|
|
2423
|
+
if (part.type === 'text' && part.synthetic === true) {
|
|
2424
|
+
return
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
if (part.type === 'text' && part.ignored === true) {
|
|
2428
|
+
await this.sendPartMessage({ part, repulseTyping: false })
|
|
2429
|
+
return
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2206
2432
|
this.storePart(part)
|
|
2207
2433
|
|
|
2208
2434
|
if (messageKind === 'unknown') {
|
|
@@ -2236,11 +2462,8 @@ export class ThreadSessionRuntime {
|
|
|
2236
2462
|
await this.flushCurrentTurnParts({
|
|
2237
2463
|
mode: 'progress',
|
|
2238
2464
|
})
|
|
2239
|
-
const unsent = this.getCurrentTurnParts().filter((candidate) => {
|
|
2240
|
-
return !this.state?.sentPartIds.has(candidate.id)
|
|
2241
|
-
})
|
|
2242
2465
|
const held = planAssistantTurnFlush({
|
|
2243
|
-
parts:
|
|
2466
|
+
parts: this.getCurrentTurnParts(),
|
|
2244
2467
|
mode: 'progress',
|
|
2245
2468
|
}).hold.some((entry) => entry.id === part.id)
|
|
2246
2469
|
if (held) {
|
|
@@ -2607,6 +2830,12 @@ export class ThreadSessionRuntime {
|
|
|
2607
2830
|
mode: 'final',
|
|
2608
2831
|
repulseTyping: false,
|
|
2609
2832
|
})
|
|
2833
|
+
await this.unquoteFinalTextPart()
|
|
2834
|
+
|
|
2835
|
+
await this.maybeNotifyPromptCacheClear({
|
|
2836
|
+
sessionId,
|
|
2837
|
+
messageId: completedMessageId,
|
|
2838
|
+
})
|
|
2610
2839
|
|
|
2611
2840
|
// Skip footer if model produced no visible output (no text, no tool calls,
|
|
2612
2841
|
// just step-start/step-finish lifecycle parts). This happens when the model
|
|
@@ -3401,10 +3630,12 @@ export class ThreadSessionRuntime {
|
|
|
3401
3630
|
})()
|
|
3402
3631
|
const worktreeChanged = this.consumeWorktreePromptChange(worktree)
|
|
3403
3632
|
const syntheticContext = getOpencodePromptContext({
|
|
3633
|
+
sessionId: session.id,
|
|
3634
|
+
threadId: this.thread.id,
|
|
3404
3635
|
username: input.username,
|
|
3405
3636
|
userId: input.userId,
|
|
3406
3637
|
sourceMessageId: input.sourceMessageId,
|
|
3407
|
-
sourceThreadId: input.sourceThreadId,
|
|
3638
|
+
sourceThreadId: input.sourceThreadId || this.thread.id,
|
|
3408
3639
|
threadName: this.thread.name || undefined,
|
|
3409
3640
|
repliedMessage: input.repliedMessage,
|
|
3410
3641
|
worktree,
|
|
@@ -3538,7 +3769,19 @@ export class ThreadSessionRuntime {
|
|
|
3538
3769
|
let result: EnqueueResult = { queued: false, queueId }
|
|
3539
3770
|
|
|
3540
3771
|
await this.dispatchAction(async () => {
|
|
3541
|
-
|
|
3772
|
+
const persistResult = await insertThreadQueueItem({
|
|
3773
|
+
queueId,
|
|
3774
|
+
threadId: this.threadId,
|
|
3775
|
+
payloadJson: JSON.stringify(queuedMessage),
|
|
3776
|
+
}).catch((error) => {
|
|
3777
|
+
return new Error('Failed to persist queued message', { cause: error })
|
|
3778
|
+
})
|
|
3779
|
+
if (persistResult instanceof Error) {
|
|
3780
|
+
logger.error(
|
|
3781
|
+
`[QUEUE] Failed to persist queued message ${queueId} in thread ${this.threadId}: ${persistResult.message}`,
|
|
3782
|
+
)
|
|
3783
|
+
throw persistResult
|
|
3784
|
+
}
|
|
3542
3785
|
threadState.enqueueItem(this.threadId, queuedMessage)
|
|
3543
3786
|
|
|
3544
3787
|
// Determine if the message is genuinely waiting in queue
|
|
@@ -3547,7 +3790,7 @@ export class ThreadSessionRuntime {
|
|
|
3547
3790
|
const willDrainNow = stateAfterEnqueue
|
|
3548
3791
|
? (
|
|
3549
3792
|
stateAfterEnqueue.queueItems.length > 0
|
|
3550
|
-
&& !this.
|
|
3793
|
+
&& !this.isBusy()
|
|
3551
3794
|
)
|
|
3552
3795
|
: false
|
|
3553
3796
|
result = !willDrainNow && position > 0
|
|
@@ -3780,7 +4023,7 @@ export class ThreadSessionRuntime {
|
|
|
3780
4023
|
}
|
|
3781
4024
|
|
|
3782
4025
|
const sessionId = state.sessionId
|
|
3783
|
-
const sessionIsBusy = this.
|
|
4026
|
+
const sessionIsBusy = this.isBusy()
|
|
3784
4027
|
|
|
3785
4028
|
logger.log(
|
|
3786
4029
|
`[ABORT] id=${abortId} reason=${reason} threadId=${this.threadId} sessionId=${sessionId || 'none'} queueLength=${state.queueItems.length} ${this.formatRunStateForLog()} sessionBusy=${sessionIsBusy}`,
|
|
@@ -3837,7 +4080,7 @@ export class ThreadSessionRuntime {
|
|
|
3837
4080
|
let needsIdleWait = false
|
|
3838
4081
|
const waitSinceTimestamp = Date.now()
|
|
3839
4082
|
const abortResult = await this.dispatchAction(async () => {
|
|
3840
|
-
needsIdleWait = this.
|
|
4083
|
+
needsIdleWait = this.isBusy()
|
|
3841
4084
|
const outcome = this.abortActiveRunInternal({ reason })
|
|
3842
4085
|
if (outcome.apiAbortPromise) {
|
|
3843
4086
|
void outcome.apiAbortPromise
|
|
@@ -3866,18 +4109,72 @@ export class ThreadSessionRuntime {
|
|
|
3866
4109
|
}
|
|
3867
4110
|
|
|
3868
4111
|
/** Clear all queued messages. Returns the removed items. */
|
|
3869
|
-
clearQueue(): threadState.QueuedMessage[] {
|
|
3870
|
-
|
|
4112
|
+
async clearQueue(): Promise<threadState.QueuedMessage[]> {
|
|
4113
|
+
let cleared: threadState.QueuedMessage[] = []
|
|
4114
|
+
await this.dispatchAction(async () => {
|
|
4115
|
+
const persistResult = await deleteThreadQueueItems(this.threadId).catch((error) => {
|
|
4116
|
+
return new Error('Failed to clear persisted queue', { cause: error })
|
|
4117
|
+
})
|
|
4118
|
+
if (persistResult instanceof Error) {
|
|
4119
|
+
logger.error(
|
|
4120
|
+
`[QUEUE] Failed to clear persisted queue for thread ${this.threadId}: ${persistResult.message}`,
|
|
4121
|
+
)
|
|
4122
|
+
return
|
|
4123
|
+
}
|
|
4124
|
+
cleared = threadState.clearQueueItems(this.threadId)
|
|
4125
|
+
})
|
|
4126
|
+
return cleared
|
|
3871
4127
|
}
|
|
3872
4128
|
|
|
3873
4129
|
/** Remove a queued message by its 1-based position. */
|
|
3874
|
-
removeQueuePosition(position: number): threadState.QueuedMessage | undefined {
|
|
3875
|
-
|
|
4130
|
+
async removeQueuePosition(position: number): Promise<threadState.QueuedMessage | undefined> {
|
|
4131
|
+
let removed: threadState.QueuedMessage | undefined
|
|
4132
|
+
await this.dispatchAction(async () => {
|
|
4133
|
+
const current = this.state?.queueItems[position - 1]
|
|
4134
|
+
if (!current) {
|
|
4135
|
+
return
|
|
4136
|
+
}
|
|
4137
|
+
if (current.queueId) {
|
|
4138
|
+
const persistResult = await deleteThreadQueueItem(current.queueId).catch((error) => {
|
|
4139
|
+
return new Error('Failed to delete persisted queue item', { cause: error })
|
|
4140
|
+
})
|
|
4141
|
+
if (persistResult instanceof Error) {
|
|
4142
|
+
logger.error(
|
|
4143
|
+
`[QUEUE] Failed to delete persisted queue item ${current.queueId}: ${persistResult.message}`,
|
|
4144
|
+
)
|
|
4145
|
+
return
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
removed = threadState.removeQueueItemAtPosition(this.threadId, position)
|
|
4149
|
+
})
|
|
4150
|
+
return removed
|
|
3876
4151
|
}
|
|
3877
4152
|
|
|
3878
4153
|
/** Remove a queued message by stable queue id. */
|
|
3879
|
-
removeQueueItemById(queueId: string): threadState.QueuedMessage | undefined {
|
|
3880
|
-
|
|
4154
|
+
async removeQueueItemById(queueId: string): Promise<threadState.QueuedMessage | undefined> {
|
|
4155
|
+
let removed: threadState.QueuedMessage | undefined
|
|
4156
|
+
await this.dispatchAction(async () => {
|
|
4157
|
+
const persistResult = await deleteThreadQueueItem(queueId).catch((error) => {
|
|
4158
|
+
return new Error('Failed to delete persisted queue item', { cause: error })
|
|
4159
|
+
})
|
|
4160
|
+
if (persistResult instanceof Error) {
|
|
4161
|
+
logger.error(
|
|
4162
|
+
`[QUEUE] Failed to delete persisted queue item ${queueId}: ${persistResult.message}`,
|
|
4163
|
+
)
|
|
4164
|
+
return
|
|
4165
|
+
}
|
|
4166
|
+
removed = threadState.removeQueueItemById(this.threadId, queueId)
|
|
4167
|
+
if (!removed && persistResult) {
|
|
4168
|
+
const parsed = parseQueuedMessagePayload({
|
|
4169
|
+
queueId: persistResult.queue_id,
|
|
4170
|
+
payloadJson: persistResult.payload_json,
|
|
4171
|
+
})
|
|
4172
|
+
if (!(parsed instanceof Error)) {
|
|
4173
|
+
removed = parsed
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
})
|
|
4177
|
+
return removed
|
|
3881
4178
|
}
|
|
3882
4179
|
|
|
3883
4180
|
/**
|
|
@@ -3886,33 +4183,150 @@ export class ThreadSessionRuntime {
|
|
|
3886
4183
|
* Returns { found: true, removed } if the item was in the queue,
|
|
3887
4184
|
* or { found: false } if it was already dispatched or never queued.
|
|
3888
4185
|
*/
|
|
3889
|
-
updateQueuedMessage(
|
|
4186
|
+
async updateQueuedMessage(
|
|
3890
4187
|
sourceMessageId: string,
|
|
3891
4188
|
newPrompt: string,
|
|
3892
|
-
): { found: boolean; removed: boolean } {
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
4189
|
+
): Promise<{ found: boolean; removed: boolean }> {
|
|
4190
|
+
let result: { found: boolean; removed: boolean } = { found: false, removed: false }
|
|
4191
|
+
await this.dispatchAction(async () => {
|
|
4192
|
+
const trimmed = newPrompt.trim()
|
|
4193
|
+
const original = this.state?.queueItems.find((item) => {
|
|
4194
|
+
return item.sourceMessageId === sourceMessageId
|
|
4195
|
+
})
|
|
4196
|
+
if (!original) {
|
|
4197
|
+
result = { found: false, removed: false }
|
|
4198
|
+
return
|
|
4199
|
+
}
|
|
4200
|
+
const queueId = original.queueId
|
|
4201
|
+
if (queueId) {
|
|
4202
|
+
const persistResult = trimmed
|
|
4203
|
+
? await updateThreadQueueItemPayload({
|
|
4204
|
+
queueId,
|
|
4205
|
+
payloadJson: JSON.stringify({ ...original, prompt: trimmed }),
|
|
4206
|
+
}).catch((error) => {
|
|
4207
|
+
return new Error('Failed to update persisted queue item', { cause: error })
|
|
4208
|
+
})
|
|
4209
|
+
: await deleteThreadQueueItem(queueId).catch((error) => {
|
|
4210
|
+
return new Error('Failed to delete persisted queue item', { cause: error })
|
|
4211
|
+
})
|
|
4212
|
+
if (persistResult instanceof Error) {
|
|
4213
|
+
logger.error(
|
|
4214
|
+
`[QUEUE] Failed to persist queue update for ${queueId}: ${persistResult.message}`,
|
|
4215
|
+
)
|
|
4216
|
+
result = { found: true, removed: false }
|
|
4217
|
+
return
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
threadState.updateQueueItemBySourceMessageId(
|
|
4221
|
+
this.threadId,
|
|
4222
|
+
sourceMessageId,
|
|
4223
|
+
(item) => {
|
|
4224
|
+
if (!trimmed) return null
|
|
4225
|
+
return { ...item, prompt: trimmed }
|
|
4226
|
+
},
|
|
4227
|
+
)
|
|
4228
|
+
result = trimmed
|
|
4229
|
+
? { found: true, removed: false }
|
|
4230
|
+
: { found: true, removed: true }
|
|
4231
|
+
})
|
|
4232
|
+
return result
|
|
3905
4233
|
}
|
|
3906
4234
|
|
|
3907
4235
|
/** Remove a queued message identified by its Discord source message ID. */
|
|
3908
|
-
removeQueuedMessage(
|
|
4236
|
+
async removeQueuedMessage(
|
|
3909
4237
|
sourceMessageId: string,
|
|
3910
|
-
): threadState.QueuedMessage | undefined {
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
4238
|
+
): Promise<threadState.QueuedMessage | undefined> {
|
|
4239
|
+
let removed: threadState.QueuedMessage | undefined
|
|
4240
|
+
await this.dispatchAction(async () => {
|
|
4241
|
+
const current = this.state?.queueItems.find((item) => {
|
|
4242
|
+
return item.sourceMessageId === sourceMessageId
|
|
4243
|
+
})
|
|
4244
|
+
if (!current) {
|
|
4245
|
+
return
|
|
4246
|
+
}
|
|
4247
|
+
if (current.queueId) {
|
|
4248
|
+
const persistResult = await deleteThreadQueueItem(current.queueId).catch((error) => {
|
|
4249
|
+
return new Error('Failed to delete persisted queue item', { cause: error })
|
|
4250
|
+
})
|
|
4251
|
+
if (persistResult instanceof Error) {
|
|
4252
|
+
logger.error(
|
|
4253
|
+
`[QUEUE] Failed to delete persisted queue item ${current.queueId}: ${persistResult.message}`,
|
|
4254
|
+
)
|
|
4255
|
+
return
|
|
4256
|
+
}
|
|
4257
|
+
}
|
|
4258
|
+
removed = threadState.updateQueueItemBySourceMessageId(
|
|
4259
|
+
this.threadId,
|
|
4260
|
+
sourceMessageId,
|
|
4261
|
+
() => null,
|
|
4262
|
+
)
|
|
4263
|
+
})
|
|
4264
|
+
return removed
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
async mergeRestoredQueueAndDrain(items: QueuedMessage[]): Promise<void> {
|
|
4268
|
+
const current = this.state?.queueItems ?? []
|
|
4269
|
+
const currentIds = new Set(current.flatMap((item) => item.queueId ? [item.queueId] : []))
|
|
4270
|
+
threadState.replaceQueueItems(this.threadId, [
|
|
4271
|
+
...items.filter((item) => item.queueId && !currentIds.has(item.queueId)),
|
|
4272
|
+
...current,
|
|
4273
|
+
])
|
|
4274
|
+
const liveStatus = await this.hydrateLiveSessionStatus()
|
|
4275
|
+
if (liveStatus === 'unavailable') {
|
|
4276
|
+
return
|
|
4277
|
+
}
|
|
4278
|
+
await this.tryDrainQueue({ showIndicator: true })
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
private async hydrateLiveSessionStatus(): Promise<'idle' | 'busy' | 'unavailable'> {
|
|
4282
|
+
const sessionId = this.state?.sessionId
|
|
4283
|
+
if (!sessionId) {
|
|
4284
|
+
return 'idle'
|
|
4285
|
+
}
|
|
4286
|
+
await this.hydrateSessionEventsFromDatabase({ sessionId })
|
|
4287
|
+
const getClient = await initializeOpencodeForDirectory(this.sdkDirectory)
|
|
4288
|
+
if (getClient instanceof Error) {
|
|
4289
|
+
logger.warn(
|
|
4290
|
+
`[QUEUE] OpenCode unavailable while restoring queue for ${this.threadId}: ${getClient.message}`,
|
|
4291
|
+
)
|
|
4292
|
+
return 'unavailable'
|
|
4293
|
+
}
|
|
4294
|
+
const statusResponse = await getClient().session.status({
|
|
4295
|
+
directory: this.sdkDirectory,
|
|
4296
|
+
}).catch((error) => {
|
|
4297
|
+
logger.warn(
|
|
4298
|
+
`[QUEUE] Failed to read session status while restoring queue for ${this.threadId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
4299
|
+
)
|
|
4300
|
+
return undefined
|
|
4301
|
+
})
|
|
4302
|
+
if (!statusResponse || statusResponse.error) {
|
|
4303
|
+
return 'unavailable'
|
|
4304
|
+
}
|
|
4305
|
+
const sessionStatus = statusResponse.data?.[sessionId]
|
|
4306
|
+
if (!sessionStatus || sessionStatus.type === 'idle') {
|
|
4307
|
+
this.markQueueDispatchIdle(sessionId)
|
|
4308
|
+
return 'idle'
|
|
4309
|
+
}
|
|
4310
|
+
this.markQueueDispatchBusy(sessionId)
|
|
4311
|
+
return 'busy'
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
private async acknowledgeAcceptedQueueItem(item: QueuedMessage): Promise<void> {
|
|
4315
|
+
if (!item.queueId) {
|
|
4316
|
+
return
|
|
4317
|
+
}
|
|
4318
|
+
await this.dispatchAction(async () => {
|
|
4319
|
+
const persistResult = await deleteThreadQueueItem(item.queueId!).catch((error) => {
|
|
4320
|
+
return new Error('Failed to delete persisted queue item', { cause: error })
|
|
4321
|
+
})
|
|
4322
|
+
if (persistResult instanceof Error) {
|
|
4323
|
+
logger.error(
|
|
4324
|
+
`[QUEUE] Failed to persist accept of ${item.queueId}: ${persistResult.message}`,
|
|
4325
|
+
)
|
|
4326
|
+
return
|
|
4327
|
+
}
|
|
4328
|
+
threadState.removeQueueItemById(this.threadId, item.queueId!)
|
|
4329
|
+
})
|
|
3916
4330
|
}
|
|
3917
4331
|
|
|
3918
4332
|
// ── Queue Drain ─────────────────────────────────────────────
|
|
@@ -3947,10 +4361,11 @@ export class ThreadSessionRuntime {
|
|
|
3947
4361
|
return
|
|
3948
4362
|
}
|
|
3949
4363
|
|
|
3950
|
-
const next =
|
|
4364
|
+
const next = thread.queueItems.find((item) => item.queueId !== this.dispatchingQueueId)
|
|
3951
4365
|
if (!next) {
|
|
3952
4366
|
return
|
|
3953
4367
|
}
|
|
4368
|
+
this.dispatchingQueueId = next.queueId
|
|
3954
4369
|
|
|
3955
4370
|
logger.log(
|
|
3956
4371
|
`[QUEUE DRAIN] Processing queued message from ${next.username}`,
|
|
@@ -3979,13 +4394,23 @@ export class ThreadSessionRuntime {
|
|
|
3979
4394
|
if (dispatchSessionId) {
|
|
3980
4395
|
this.markQueueDispatchBusy(dispatchSessionId)
|
|
3981
4396
|
}
|
|
3982
|
-
|
|
4397
|
+
let accepted = false
|
|
4398
|
+
void this.dispatchPrompt(next).then(async (ok) => {
|
|
4399
|
+
accepted = ok
|
|
4400
|
+
if (ok) {
|
|
4401
|
+
await this.acknowledgeAcceptedQueueItem(next)
|
|
4402
|
+
}
|
|
4403
|
+
}).catch(async (err) => {
|
|
3983
4404
|
logger.error('[DISPATCH] Prompt dispatch failed:', err)
|
|
3984
4405
|
void notifyError(err, 'Runtime prompt dispatch failed')
|
|
3985
4406
|
if (dispatchSessionId) {
|
|
3986
4407
|
this.markQueueDispatchIdle(dispatchSessionId)
|
|
3987
4408
|
}
|
|
3988
4409
|
}).finally(() => {
|
|
4410
|
+
this.dispatchingQueueId = undefined
|
|
4411
|
+
if (!accepted) {
|
|
4412
|
+
return
|
|
4413
|
+
}
|
|
3989
4414
|
void this.dispatchAction(() => {
|
|
3990
4415
|
return this.tryDrainQueue({ showIndicator: true })
|
|
3991
4416
|
})
|
|
@@ -3997,7 +4422,7 @@ export class ThreadSessionRuntime {
|
|
|
3997
4422
|
// The listener is already running, so this only handles
|
|
3998
4423
|
// session ensure + model/agent + SDK call + state.
|
|
3999
4424
|
|
|
4000
|
-
private async dispatchPrompt(input: QueuedMessage): Promise<
|
|
4425
|
+
private async dispatchPrompt(input: QueuedMessage): Promise<boolean> {
|
|
4001
4426
|
this.lastDisplayedContextPercentage = 0
|
|
4002
4427
|
this.lastRateLimitDisplayTime = 0
|
|
4003
4428
|
this.lastSentPartKind = undefined
|
|
@@ -4020,8 +4445,7 @@ export class ThreadSessionRuntime {
|
|
|
4020
4445
|
)
|
|
4021
4446
|
// Show indicator: this dispatch failed, so the next queued message
|
|
4022
4447
|
// has been waiting — the user needs to see which one is starting.
|
|
4023
|
-
|
|
4024
|
-
return
|
|
4448
|
+
return false
|
|
4025
4449
|
}
|
|
4026
4450
|
const { session, getClient, createdNewSession } = sessionResult
|
|
4027
4451
|
|
|
@@ -4038,8 +4462,7 @@ export class ThreadSessionRuntime {
|
|
|
4038
4462
|
`Failed to update session permissions: ${updatePermissionsResult.message}`,
|
|
4039
4463
|
{ flags: NOTIFY_MESSAGE_FLAGS },
|
|
4040
4464
|
)
|
|
4041
|
-
|
|
4042
|
-
return
|
|
4465
|
+
return false
|
|
4043
4466
|
}
|
|
4044
4467
|
|
|
4045
4468
|
// ── Resolve model + agent preferences ─────────────────────
|
|
@@ -4067,8 +4490,7 @@ export class ThreadSessionRuntime {
|
|
|
4067
4490
|
`Failed to resolve model: ${validatedModel.message}`,
|
|
4068
4491
|
{ flags: NOTIFY_MESSAGE_FLAGS },
|
|
4069
4492
|
)
|
|
4070
|
-
|
|
4071
|
-
return
|
|
4493
|
+
return false
|
|
4072
4494
|
}
|
|
4073
4495
|
}
|
|
4074
4496
|
|
|
@@ -4097,9 +4519,7 @@ export class ThreadSessionRuntime {
|
|
|
4097
4519
|
`Failed to resolve agent: ${earlyAgentResult.message}`,
|
|
4098
4520
|
{ flags: NOTIFY_MESSAGE_FLAGS },
|
|
4099
4521
|
)
|
|
4100
|
-
|
|
4101
|
-
await this.tryDrainQueue({ showIndicator: true })
|
|
4102
|
-
return
|
|
4522
|
+
return false
|
|
4103
4523
|
}
|
|
4104
4524
|
const earlyAgentPreference = earlyAgentResult.agentPreference
|
|
4105
4525
|
const earlyAvailableAgents = earlyAgentResult.agents
|
|
@@ -4148,9 +4568,7 @@ export class ThreadSessionRuntime {
|
|
|
4148
4568
|
`Failed to resolve model: ${earlyModelResult.message}`,
|
|
4149
4569
|
{ flags: NOTIFY_MESSAGE_FLAGS },
|
|
4150
4570
|
)
|
|
4151
|
-
|
|
4152
|
-
await this.tryDrainQueue({ showIndicator: true })
|
|
4153
|
-
return
|
|
4571
|
+
return false
|
|
4154
4572
|
}
|
|
4155
4573
|
const earlyModelParam = earlyModelResult
|
|
4156
4574
|
if (!earlyModelParam) {
|
|
@@ -4159,9 +4577,7 @@ export class ThreadSessionRuntime {
|
|
|
4159
4577
|
this.thread,
|
|
4160
4578
|
'No AI provider connected. Configure a provider in OpenCode with `/connect` command.',
|
|
4161
4579
|
)
|
|
4162
|
-
|
|
4163
|
-
await this.tryDrainQueue({ showIndicator: true })
|
|
4164
|
-
return
|
|
4580
|
+
return false
|
|
4165
4581
|
}
|
|
4166
4582
|
|
|
4167
4583
|
// Resolve thinking variant
|
|
@@ -4243,10 +4659,12 @@ export class ThreadSessionRuntime {
|
|
|
4243
4659
|
})()
|
|
4244
4660
|
const worktreeChanged = this.consumeWorktreePromptChange(worktree)
|
|
4245
4661
|
const syntheticContext = getOpencodePromptContext({
|
|
4662
|
+
sessionId: session.id,
|
|
4663
|
+
threadId: this.thread.id,
|
|
4246
4664
|
username: input.username,
|
|
4247
4665
|
userId: input.userId,
|
|
4248
4666
|
sourceMessageId: input.sourceMessageId,
|
|
4249
|
-
sourceThreadId: input.sourceThreadId,
|
|
4667
|
+
sourceThreadId: input.sourceThreadId || this.thread.id,
|
|
4250
4668
|
threadName: this.thread.name || undefined,
|
|
4251
4669
|
repliedMessage: input.repliedMessage,
|
|
4252
4670
|
worktree,
|
|
@@ -4294,10 +4712,12 @@ export class ThreadSessionRuntime {
|
|
|
4294
4712
|
// Append <discord-user /> tag to arguments so external sync can
|
|
4295
4713
|
// detect this message came from Discord (same tag as promptAsync).
|
|
4296
4714
|
const discordTag = getOpencodePromptContext({
|
|
4715
|
+
sessionId: session.id,
|
|
4716
|
+
threadId: this.thread.id,
|
|
4297
4717
|
username: input.username,
|
|
4298
4718
|
userId: input.userId,
|
|
4299
4719
|
sourceMessageId: input.sourceMessageId,
|
|
4300
|
-
sourceThreadId: input.sourceThreadId,
|
|
4720
|
+
sourceThreadId: input.sourceThreadId || this.thread.id,
|
|
4301
4721
|
threadName: this.thread.name || undefined,
|
|
4302
4722
|
repliedMessage: input.repliedMessage,
|
|
4303
4723
|
})
|
|
@@ -4341,10 +4761,7 @@ export class ThreadSessionRuntime {
|
|
|
4341
4761
|
`✗ Failed to prepare command system prompt: ${systemWriteResult.message}`,
|
|
4342
4762
|
{ flags: NOTIFY_MESSAGE_FLAGS },
|
|
4343
4763
|
)
|
|
4344
|
-
|
|
4345
|
-
return this.tryDrainQueue({ showIndicator: true })
|
|
4346
|
-
})
|
|
4347
|
-
return
|
|
4764
|
+
return false
|
|
4348
4765
|
}
|
|
4349
4766
|
const commandResponse = await getClient().session.command(
|
|
4350
4767
|
{
|
|
@@ -4376,10 +4793,7 @@ export class ThreadSessionRuntime {
|
|
|
4376
4793
|
'✗ Command timed out after 30 seconds. Try a shorter command or run it with /run-shell-command.',
|
|
4377
4794
|
{ flags: NOTIFY_MESSAGE_FLAGS },
|
|
4378
4795
|
)
|
|
4379
|
-
|
|
4380
|
-
return this.tryDrainQueue({ showIndicator: true })
|
|
4381
|
-
})
|
|
4382
|
-
return
|
|
4796
|
+
return false
|
|
4383
4797
|
}
|
|
4384
4798
|
|
|
4385
4799
|
const commandErrorForAbortCheck: unknown = commandResponse
|
|
@@ -4388,7 +4802,7 @@ export class ThreadSessionRuntime {
|
|
|
4388
4802
|
`[DISPATCH] Command aborted (expected) sessionId=${session.id}`,
|
|
4389
4803
|
)
|
|
4390
4804
|
this.stopTyping()
|
|
4391
|
-
return
|
|
4805
|
+
return true
|
|
4392
4806
|
}
|
|
4393
4807
|
|
|
4394
4808
|
logger.error(
|
|
@@ -4401,10 +4815,7 @@ export class ThreadSessionRuntime {
|
|
|
4401
4815
|
`✗ Unexpected bot Error: ${commandResponse.message}`,
|
|
4402
4816
|
{ flags: NOTIFY_MESSAGE_FLAGS },
|
|
4403
4817
|
)
|
|
4404
|
-
|
|
4405
|
-
return this.tryDrainQueue({ showIndicator: true })
|
|
4406
|
-
})
|
|
4407
|
-
return
|
|
4818
|
+
return false
|
|
4408
4819
|
}
|
|
4409
4820
|
|
|
4410
4821
|
if (commandResponse.error) {
|
|
@@ -4414,7 +4825,7 @@ export class ThreadSessionRuntime {
|
|
|
4414
4825
|
`[DISPATCH] Command aborted (expected) sessionId=${session.id}`,
|
|
4415
4826
|
)
|
|
4416
4827
|
this.stopTyping()
|
|
4417
|
-
return
|
|
4828
|
+
return true
|
|
4418
4829
|
}
|
|
4419
4830
|
const apiError = new Error(`OpenCode API error: ${errorMessage}`)
|
|
4420
4831
|
logger.error(`[DISPATCH] ${apiError.message}`)
|
|
@@ -4423,10 +4834,7 @@ export class ThreadSessionRuntime {
|
|
|
4423
4834
|
await sendThreadMessage(this.thread, `✗ ${apiError.message}`, {
|
|
4424
4835
|
flags: NOTIFY_MESSAGE_FLAGS,
|
|
4425
4836
|
})
|
|
4426
|
-
|
|
4427
|
-
return this.tryDrainQueue({ showIndicator: true })
|
|
4428
|
-
})
|
|
4429
|
-
return
|
|
4837
|
+
return false
|
|
4430
4838
|
}
|
|
4431
4839
|
|
|
4432
4840
|
logger.log(`[DISPATCH] Successfully ran command for session ${session.id}`)
|
|
@@ -4436,7 +4844,7 @@ export class ThreadSessionRuntime {
|
|
|
4436
4844
|
source: resolveTurnSource(input),
|
|
4437
4845
|
agent: earlyAgentPreference,
|
|
4438
4846
|
})
|
|
4439
|
-
return
|
|
4847
|
+
return true
|
|
4440
4848
|
}
|
|
4441
4849
|
|
|
4442
4850
|
await waitForGlobalEventListener()
|
|
@@ -4475,10 +4883,7 @@ export class ThreadSessionRuntime {
|
|
|
4475
4883
|
await sendThreadMessage(this.thread, `✗ OpenCode API error: ${errorMessage}`, {
|
|
4476
4884
|
flags: NOTIFY_MESSAGE_FLAGS,
|
|
4477
4885
|
})
|
|
4478
|
-
|
|
4479
|
-
return this.tryDrainQueue({ showIndicator: true })
|
|
4480
|
-
})
|
|
4481
|
-
return
|
|
4886
|
+
return false
|
|
4482
4887
|
}
|
|
4483
4888
|
|
|
4484
4889
|
logger.log(
|
|
@@ -4490,6 +4895,7 @@ export class ThreadSessionRuntime {
|
|
|
4490
4895
|
source: resolveTurnSource(input),
|
|
4491
4896
|
agent: earlyAgentPreference,
|
|
4492
4897
|
})
|
|
4898
|
+
return true
|
|
4493
4899
|
}
|
|
4494
4900
|
|
|
4495
4901
|
// ── Session Ensure ──────────────────────────────────────────
|
|
@@ -4768,7 +5174,7 @@ export class ThreadSessionRuntime {
|
|
|
4768
5174
|
: ''
|
|
4769
5175
|
const result = await sendThreadMessage(
|
|
4770
5176
|
this.thread,
|
|
4771
|
-
|
|
5177
|
+
asSystemLine(`*using ${modelLabel}${agentLabel}*`),
|
|
4772
5178
|
{ flags: SILENT_MESSAGE_FLAGS },
|
|
4773
5179
|
).catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }))
|
|
4774
5180
|
if (result instanceof Error) {
|
|
@@ -4921,7 +5327,7 @@ export class ThreadSessionRuntime {
|
|
|
4921
5327
|
})
|
|
4922
5328
|
: undefined
|
|
4923
5329
|
const mention = mentionUserId ? ` <@${mentionUserId}>` : ''
|
|
4924
|
-
const footerText =
|
|
5330
|
+
const footerText = asSystemLine(
|
|
4925
5331
|
`*${projectInfo}${sessionDuration}${contextInfo}${modelInfo}${agentInfo}*${mention}`,
|
|
4926
5332
|
)
|
|
4927
5333
|
this.stopTyping()
|
|
@@ -4943,6 +5349,77 @@ export class ThreadSessionRuntime {
|
|
|
4943
5349
|
this.lastSentPartKind = undefined
|
|
4944
5350
|
}
|
|
4945
5351
|
|
|
5352
|
+
private async maybeNotifyPromptCacheClear({
|
|
5353
|
+
sessionId,
|
|
5354
|
+
messageId,
|
|
5355
|
+
}: {
|
|
5356
|
+
sessionId: string
|
|
5357
|
+
messageId: string
|
|
5358
|
+
}): Promise<void> {
|
|
5359
|
+
const cacheClear = getPromptCacheClear({
|
|
5360
|
+
events: this.eventBuffer,
|
|
5361
|
+
sessionId,
|
|
5362
|
+
currentMessageId: messageId,
|
|
5363
|
+
})
|
|
5364
|
+
if (!cacheClear) {
|
|
5365
|
+
return
|
|
5366
|
+
}
|
|
5367
|
+
const systemDiff = await this.getSystemPromptDiffForCacheClear({
|
|
5368
|
+
sessionId,
|
|
5369
|
+
previousMessageId: cacheClear.previousMessageId,
|
|
5370
|
+
currentMessageId: cacheClear.currentMessageId,
|
|
5371
|
+
})
|
|
5372
|
+
const chunk = `${STATUS_PREFIX}${formatPromptCacheClearMessage(cacheClear, systemDiff)}`
|
|
5373
|
+
const sendResult = await this.thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
5374
|
+
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }))
|
|
5375
|
+
if (sendResult instanceof Error) {
|
|
5376
|
+
discordLogger.error('Failed to send prompt cache notice:', sendResult)
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
|
|
5380
|
+
private async getSystemPromptDiffForCacheClear({
|
|
5381
|
+
sessionId,
|
|
5382
|
+
previousMessageId,
|
|
5383
|
+
currentMessageId,
|
|
5384
|
+
}: {
|
|
5385
|
+
sessionId: string
|
|
5386
|
+
previousMessageId: string
|
|
5387
|
+
currentMessageId: string
|
|
5388
|
+
}): Promise<{ additions: number; deletions: number } | undefined> {
|
|
5389
|
+
const previousParentId = this.getAssistantParentId({ sessionId, messageId: previousMessageId })
|
|
5390
|
+
const currentParentId = this.getAssistantParentId({ sessionId, messageId: currentMessageId })
|
|
5391
|
+
if (!previousParentId || !currentParentId) {
|
|
5392
|
+
return undefined
|
|
5393
|
+
}
|
|
5394
|
+
const beforeText = this.userSystemByMessageId.get(previousParentId)
|
|
5395
|
+
const afterText = this.userSystemByMessageId.get(currentParentId)
|
|
5396
|
+
if (beforeText === undefined || afterText === undefined || beforeText === afterText) {
|
|
5397
|
+
return undefined
|
|
5398
|
+
}
|
|
5399
|
+
return countSystemPromptDiffLines({ beforeText, afterText })
|
|
5400
|
+
}
|
|
5401
|
+
|
|
5402
|
+
private getAssistantParentId({
|
|
5403
|
+
sessionId,
|
|
5404
|
+
messageId,
|
|
5405
|
+
}: {
|
|
5406
|
+
sessionId: string
|
|
5407
|
+
messageId: string
|
|
5408
|
+
}): string | undefined {
|
|
5409
|
+
for (let i = this.eventBuffer.length - 1; i >= 0; i--) {
|
|
5410
|
+
const event = this.eventBuffer[i]?.event
|
|
5411
|
+
if (event?.type !== 'message.updated') {
|
|
5412
|
+
continue
|
|
5413
|
+
}
|
|
5414
|
+
const info = event.properties.info
|
|
5415
|
+
if (info.sessionID !== sessionId || info.role !== 'assistant' || info.id !== messageId) {
|
|
5416
|
+
continue
|
|
5417
|
+
}
|
|
5418
|
+
return info.parentID
|
|
5419
|
+
}
|
|
5420
|
+
return undefined
|
|
5421
|
+
}
|
|
5422
|
+
|
|
4946
5423
|
// ── Retry Last User Prompt (for model-change flow) ──────────
|
|
4947
5424
|
|
|
4948
5425
|
/**
|
|
@@ -4965,7 +5442,7 @@ export class ThreadSessionRuntime {
|
|
|
4965
5442
|
let needsIdleWait = false
|
|
4966
5443
|
const waitSinceTimestamp = Date.now()
|
|
4967
5444
|
const abortResult = await this.dispatchAction(async () => {
|
|
4968
|
-
needsIdleWait = this.
|
|
5445
|
+
needsIdleWait = this.isBusy()
|
|
4969
5446
|
const outcome = this.abortActiveRunInternal({
|
|
4970
5447
|
reason: 'model-change',
|
|
4971
5448
|
})
|
|
@@ -5026,6 +5503,31 @@ export class ThreadSessionRuntime {
|
|
|
5026
5503
|
|
|
5027
5504
|
return true
|
|
5028
5505
|
}
|
|
5506
|
+
|
|
5507
|
+
/**
|
|
5508
|
+
* Resume an idle session by sending `text` as a new user turn.
|
|
5509
|
+
*
|
|
5510
|
+
* Used when a question is answered after its run was aborted elsewhere:
|
|
5511
|
+
* the original run is dead, so question.reply is a no-op. We instead feed
|
|
5512
|
+
* the answer back as a fresh prompt so opencode continues from history.
|
|
5513
|
+
*/
|
|
5514
|
+
async resumeWithText({ text }: { text: string }): Promise<boolean> {
|
|
5515
|
+
const sessionId = this.state?.sessionId
|
|
5516
|
+
if (!sessionId || this.disposed) {
|
|
5517
|
+
logger.log(`[RESUME] No session for thread ${this.threadId}`)
|
|
5518
|
+
return false
|
|
5519
|
+
}
|
|
5520
|
+
await this.enqueueIncoming({
|
|
5521
|
+
prompt: text,
|
|
5522
|
+
userId: '',
|
|
5523
|
+
username: '',
|
|
5524
|
+
appId: this.appId,
|
|
5525
|
+
mode: 'opencode',
|
|
5526
|
+
resetAssistantForNewRun: true,
|
|
5527
|
+
expectedSessionId: sessionId,
|
|
5528
|
+
})
|
|
5529
|
+
return true
|
|
5530
|
+
}
|
|
5029
5531
|
}
|
|
5030
5532
|
|
|
5031
5533
|
// ── Module-level helpers ──────────────────────────────────────────
|