kimaki 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-model.e2e.test.js +27 -19
- package/dist/analytics.js +3 -1
- package/dist/btw-prefix-detection.js +9 -0
- package/dist/btw-prefix-detection.test.js +38 -1
- package/dist/cli-commands/send.js +5 -11
- package/dist/cli-runner.js +22 -10
- package/dist/cli-runner.test.js +34 -1
- package/dist/cli-send-thread.e2e.test.js +1 -1
- package/dist/commands/abort.js +16 -4
- package/dist/commands/btw.js +147 -39
- package/dist/commands/context-usage.js +47 -0
- package/dist/commands/context-usage.test.js +49 -0
- package/dist/commands/fork.js +32 -1
- package/dist/commands/model.js +69 -14
- package/dist/commands/new-worktree.js +5 -2
- package/dist/commands/queue.js +37 -30
- package/dist/commands/session.js +7 -2
- package/dist/db.test.js +17 -7
- package/dist/discord-bot.js +70 -47
- package/dist/discord-utils.js +15 -1
- package/dist/errors.js +10 -5
- package/dist/external-directory-permissions.e2e.test.js +4 -4
- package/dist/external-directory-project-deny.e2e.test.js +2 -2
- package/dist/gateway-proxy.e2e.test.js +5 -5
- package/dist/message-formatting.js +50 -15
- package/dist/message-formatting.test.js +12 -11
- package/dist/message-preprocessing.js +53 -75
- package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +1 -1
- package/dist/queue-advanced-abort.e2e.test.js +62 -6
- package/dist/queue-advanced-action-buttons.e2e.test.js +3 -3
- package/dist/queue-advanced-footer.e2e.test.js +8 -8
- package/dist/queue-advanced-model-switch.e2e.test.js +2 -2
- 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 +146 -5
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +2 -2
- package/dist/queue-interrupt-drain.e2e.test.js +24 -14
- package/dist/queue-question-select-drain.e2e.test.js +27 -27
- package/dist/runtime-lifecycle.e2e.test.js +6 -6
- package/dist/session-handler/agent-utils.js +16 -17
- package/dist/session-handler/event-stream-state.js +24 -16
- package/dist/session-handler/event-stream-state.test.js +281 -14
- package/dist/session-handler/thread-session-runtime.js +209 -128
- package/dist/sleep.e2e.test.js +6 -6
- package/dist/system-message.js +214 -392
- package/dist/system-message.test.js +204 -380
- package/dist/task-runner.js +3 -3
- package/dist/task-schedule.js +2 -2
- package/dist/test-utils.js +32 -6
- package/dist/thread-message-queue.e2e.test.js +132 -60
- package/dist/undo-redo.e2e.test.js +3 -3
- package/dist/voice-handler.js +2 -1
- package/dist/voice-message.e2e.test.js +22 -31
- package/dist/voice-routing-input.e2e.test.js +2 -2
- package/dist/voice-routing-question.e2e.test.js +59 -2
- package/dist/voice.js +306 -206
- package/dist/voice.test.js +161 -13
- package/dist/worktree-lifecycle.e2e.test.js +7 -7
- package/package.json +6 -9
- package/skills/critique/SKILL.md +4 -0
- package/skills/holocron/SKILL.md +0 -28
- package/skills/opensrc/SKILL.md +20 -13
- package/skills/playwriter/SKILL.md +1 -1
- package/src/agent-model.e2e.test.ts +28 -19
- package/src/analytics.ts +3 -1
- package/src/btw-prefix-detection.test.ts +39 -1
- package/src/btw-prefix-detection.ts +11 -0
- package/src/cli-commands/send.ts +11 -11
- package/src/cli-runner.test.ts +37 -0
- package/src/cli-runner.ts +26 -10
- package/src/cli-send-thread.e2e.test.ts +1 -1
- package/src/commands/abort.ts +20 -4
- package/src/commands/btw.ts +151 -41
- package/src/commands/context-usage.test.ts +57 -0
- package/src/commands/context-usage.ts +59 -0
- package/src/commands/fork.ts +34 -1
- package/src/commands/model.ts +81 -14
- package/src/commands/new-worktree.ts +5 -2
- package/src/commands/queue.ts +46 -30
- package/src/commands/session.ts +10 -2
- package/src/db.test.ts +18 -6
- package/src/discord-bot.ts +72 -56
- package/src/discord-utils.ts +17 -1
- package/src/errors.ts +12 -6
- package/src/external-directory-permissions.e2e.test.ts +4 -4
- package/src/external-directory-project-deny.e2e.test.ts +2 -2
- package/src/gateway-proxy.e2e.test.ts +5 -5
- package/src/message-formatting.test.ts +12 -11
- package/src/message-formatting.ts +52 -16
- package/src/message-preprocessing.ts +69 -91
- package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +1 -1
- package/src/queue-advanced-abort.e2e.test.ts +73 -6
- package/src/queue-advanced-action-buttons.e2e.test.ts +3 -3
- package/src/queue-advanced-footer.e2e.test.ts +8 -8
- package/src/queue-advanced-model-switch.e2e.test.ts +2 -2
- 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 +164 -4
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +2 -2
- package/src/queue-interrupt-drain.e2e.test.ts +24 -13
- package/src/queue-question-select-drain.e2e.test.ts +31 -28
- package/src/runtime-lifecycle.e2e.test.ts +6 -6
- package/src/session-handler/agent-utils.ts +25 -20
- package/src/session-handler/event-stream-state.test.ts +284 -13
- package/src/session-handler/event-stream-state.ts +37 -21
- package/src/session-handler/thread-runtime-state.ts +7 -0
- package/src/session-handler/thread-session-runtime.ts +255 -155
- package/src/sleep.e2e.test.ts +6 -6
- package/src/system-message.test.ts +216 -379
- package/src/system-message.ts +253 -389
- package/src/task-runner.ts +3 -3
- package/src/task-schedule.ts +2 -2
- package/src/test-utils.ts +55 -12
- package/src/thread-message-queue.e2e.test.ts +136 -63
- package/src/undo-redo.e2e.test.ts +3 -3
- package/src/voice-handler.ts +2 -1
- package/src/voice-message.e2e.test.ts +22 -33
- package/src/voice-routing-input.e2e.test.ts +2 -2
- package/src/voice-routing-question.e2e.test.ts +59 -2
- package/src/voice.test.ts +164 -14
- package/src/voice.ts +400 -279
- package/src/worktree-lifecycle.e2e.test.ts +7 -7
|
@@ -17,7 +17,7 @@ import { isAbortError } from '../utils.js';
|
|
|
17
17
|
import { registerEventListener, unregisterEventListener, waitForGlobalEventListener, } from './global-event-listener.js';
|
|
18
18
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
19
19
|
import { sendThreadMessage, sendSessionPartMessage, SILENT_MESSAGE_FLAGS, NOTIFY_MESSAGE_FLAGS, raceDiscordRename, DISCORD_THREAD_RENAME_TIMEOUT_MS, resolveThreadFooterMentionUserId, resolveWorkingDirectory, } from '../discord-utils.js';
|
|
20
|
-
import { asDiscordQuote,
|
|
20
|
+
import { asDiscordQuote, asSubtext, formatPart, formatTaskToolTitle, planAssistantTurnFlush, sessionPartContent, sessionPartKind, shouldLeadWithBlankLine, shouldQuoteIntermediateTextPart, STATUS_PREFIX, WORKTREE_PREFIX, } from '../message-formatting.js';
|
|
21
21
|
import { getChannelVerbosity, getPartMessageIds, getDb, setPartMessage, getThreadSession, setThreadSession, getThreadParentSessionId, setThreadParentSessionId, getThreadWorktreeOrWorkspace, setSessionAgent, setSessionModel, clearSessionModel, getVariantCascade, setSessionStartSource, getSessionStartSource, getScheduledTask, completeScheduledTaskRunsForSession, failScheduledTaskRunsForSession, startScheduledTaskRunSession, appendSessionEventsSinceLastTimestamp, getSessionEventSnapshot, cancelSessionSleepForThread, insertThreadQueueItem, listAllThreadQueueItems, deleteThreadQueueItem, deleteThreadQueueItems, updateThreadQueueItemPayload, } from '../database.js';
|
|
22
22
|
import * as orm from 'drizzle-orm';
|
|
23
23
|
import * as schema from '../schema.js';
|
|
@@ -27,14 +27,14 @@ import { showActionButtons, waitForQueuedActionButtonsRequest, pendingActionButt
|
|
|
27
27
|
import { pendingFileUploadContexts, cancelPendingFileUpload, } from '../commands/file-upload.js';
|
|
28
28
|
import { getCurrentModelInfo, ensureSessionPreferencesSnapshot, } from '../commands/model.js';
|
|
29
29
|
import { displayedModelLabel, resolveDisplayedModelName, validateModelId, } from './model-utils.js';
|
|
30
|
-
import { getOpencodePromptContext, getOpencodeSystemMessage,
|
|
30
|
+
import { getOpencodePromptContext, getOpencodeSystemMessage, isSystemPromptForSession, resolveSessionSystemPrompt, systemPromptHasParentSession, } from '../system-message.js';
|
|
31
31
|
import { getDataDir } from '../config.js';
|
|
32
32
|
import { countSystemPromptDiffLines } from '../cache-rewrite.js';
|
|
33
33
|
import { store } from '../store.js';
|
|
34
34
|
import { trackEvent, } from '../analytics.js';
|
|
35
35
|
import { resolveValidatedAgentPreference } from './agent-utils.js';
|
|
36
36
|
import { appendOpencodeSessionEventLog, getOpencodeEventSessionId, isOpencodeSessionEventLogEnabled, } from './opencode-session-event-log.js';
|
|
37
|
-
import { doesLatestUserTurnHaveNaturalCompletion, didLatestUserTurnUseSleepTool, didQuestionQueueHandoffSinceLatestQuestionAsked, deriveLatestUnansweredQuestion, getAssistantMessageIdsForLatestUserTurn, getCurrentTurnStartTime, isSessionBusy, getLatestRunInfo, getPromptCacheClear, formatPromptCacheClearMessage, getIdleTokenUsageDelta,
|
|
37
|
+
import { doesLatestUserTurnHaveNaturalCompletion, didLatestUserTurnUseSleepTool, didQuestionQueueHandoffSinceLatestQuestionAsked, deriveLatestUnansweredQuestion, getAssistantMessageIdsForLatestUserTurn, getCurrentTurnStartTime, isSessionBusy, getLatestRunInfo, getPromptCacheClear, formatPromptCacheClearMessage, getIdleTokenUsageDelta, getDerivedSubtaskLabel, getTokenUsageSessionIdsForIdle, isDerivedChildSession, getLatestAssistantMessageIdForLatestUserTurn, getAssistantMessageKind, hasAssistantMessageCompletedBefore, isAssistantMessageInLatestUserTurn, isAssistantMessageNaturalCompletion, shouldBufferSessionEvent, shouldRetainSessionEvent, trimEventBuffer, } from './event-stream-state.js';
|
|
38
38
|
// Track multiple pending permissions per thread (keyed by permission ID).
|
|
39
39
|
// OpenCode handles blocking/sequencing — we just need to track all pending
|
|
40
40
|
// permissions to avoid duplicates and properly clean up on reply/teardown.
|
|
@@ -789,17 +789,13 @@ export class ThreadSessionRuntime {
|
|
|
789
789
|
})) {
|
|
790
790
|
return undefined;
|
|
791
791
|
}
|
|
792
|
-
const
|
|
792
|
+
const label = getDerivedSubtaskLabel({
|
|
793
793
|
events: this.eventBuffer,
|
|
794
794
|
mainSessionId,
|
|
795
795
|
candidateSessionId,
|
|
796
796
|
});
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
mainSessionId,
|
|
800
|
-
candidateSessionId,
|
|
801
|
-
});
|
|
802
|
-
const label = `${agentType || 'task'}-${subtaskIndex || 1}`;
|
|
797
|
+
if (!label)
|
|
798
|
+
return undefined;
|
|
803
799
|
const assistantMessageId = this.getLatestAssistantMessageIdForCurrentTurn({
|
|
804
800
|
sessionId: candidateSessionId,
|
|
805
801
|
});
|
|
@@ -1005,6 +1001,15 @@ export class ThreadSessionRuntime {
|
|
|
1005
1001
|
});
|
|
1006
1002
|
this.persistEventBufferDebounced.trigger();
|
|
1007
1003
|
}
|
|
1004
|
+
seedForkPromptCacheBaseline(message) {
|
|
1005
|
+
if (message.sessionID !== this.state?.sessionId || this.eventBuffer.length > 0)
|
|
1006
|
+
return;
|
|
1007
|
+
this.appendEventToBuffer({
|
|
1008
|
+
id: `fork-cache-baseline-${message.id}`,
|
|
1009
|
+
type: 'message.updated',
|
|
1010
|
+
properties: { sessionID: message.sessionID, info: message },
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1008
1013
|
// Queue-dispatch lifecycle markers are synthetic buffer-only events.
|
|
1009
1014
|
// They are not fed into handleEvent(), so they do not emit Discord messages;
|
|
1010
1015
|
// they only stabilize event-derived busy/idle gating for local queue drains.
|
|
@@ -1422,13 +1427,10 @@ export class ThreadSessionRuntime {
|
|
|
1422
1427
|
}
|
|
1423
1428
|
async unquoteFinalTextPart() {
|
|
1424
1429
|
const parts = this.getCurrentTurnParts();
|
|
1425
|
-
const finalPart =
|
|
1426
|
-
|
|
1427
|
-
mode: 'final',
|
|
1428
|
-
}).sendParts.at(-1);
|
|
1429
|
-
if (!finalPart || finalPart.quoteText || finalPart.part.type !== 'text')
|
|
1430
|
+
const finalPart = parts.findLast((part) => part.type === 'text' || part.type === 'tool');
|
|
1431
|
+
if (!finalPart || finalPart.type !== 'text')
|
|
1430
1432
|
return;
|
|
1431
|
-
const last = finalPart
|
|
1433
|
+
const last = finalPart;
|
|
1432
1434
|
const db = await getDb();
|
|
1433
1435
|
const row = await db.query.part_messages.findFirst({
|
|
1434
1436
|
where: { part_id: last.id },
|
|
@@ -1653,14 +1655,15 @@ export class ThreadSessionRuntime {
|
|
|
1653
1655
|
upToIndex: this.eventBuffer.length - 2,
|
|
1654
1656
|
});
|
|
1655
1657
|
const completedAt = msg.time.completed;
|
|
1656
|
-
if (!wasAlreadyCompleted
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1658
|
+
if (!wasAlreadyCompleted && typeof completedAt === 'number') {
|
|
1659
|
+
if (isAssistantMessageNaturalCompletion({ message: msg })) {
|
|
1660
|
+
await this.handleNaturalAssistantCompletion({
|
|
1661
|
+
completedMessageId: msg.id,
|
|
1662
|
+
completedAt,
|
|
1663
|
+
});
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
await this.maybeNotifyPromptCacheClear({ sessionId, messageId: msg.id });
|
|
1664
1667
|
}
|
|
1665
1668
|
// Context usage notice.
|
|
1666
1669
|
// Skip the final assistant update for a run: by the time the last
|
|
@@ -1693,7 +1696,7 @@ export class ThreadSessionRuntime {
|
|
|
1693
1696
|
return;
|
|
1694
1697
|
}
|
|
1695
1698
|
this.lastDisplayedContextPercentage = thresholdCrossed;
|
|
1696
|
-
const chunk =
|
|
1699
|
+
const chunk = asSubtext(`context usage ${currentPercentage}%`);
|
|
1697
1700
|
const sendResult = await this.thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
1698
1701
|
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }));
|
|
1699
1702
|
if (sendResult instanceof Error) {
|
|
@@ -1877,7 +1880,7 @@ export class ThreadSessionRuntime {
|
|
|
1877
1880
|
}
|
|
1878
1881
|
return ` (${pct.toFixed(1)}%)`;
|
|
1879
1882
|
})();
|
|
1880
|
-
const chunk = `${STATUS_PREFIX}${part.tool} returned ${formattedTokens} tokens${percentageSuffix}
|
|
1883
|
+
const chunk = asSubtext(`${STATUS_PREFIX}${part.tool} returned ${formattedTokens} tokens${percentageSuffix}`);
|
|
1881
1884
|
const largeOutputResult = await this.thread.send({
|
|
1882
1885
|
content: chunk,
|
|
1883
1886
|
flags: SILENT_MESSAGE_FLAGS,
|
|
@@ -2335,16 +2338,14 @@ export class ThreadSessionRuntime {
|
|
|
2335
2338
|
logger.log(`[QUESTION QUEUE HANDOFF] Session changed before queue handoff for thread ${this.threadId}`);
|
|
2336
2339
|
return;
|
|
2337
2340
|
}
|
|
2341
|
+
// Only a prompt can answer a pending question; a fork must wait for idle.
|
|
2342
|
+
if (this.state?.queueItems[0]?.queuedAction === 'btw')
|
|
2343
|
+
return;
|
|
2338
2344
|
const next = threadState.dequeueItem(this.threadId);
|
|
2339
2345
|
if (!next) {
|
|
2340
2346
|
return;
|
|
2341
2347
|
}
|
|
2342
|
-
|
|
2343
|
-
? `/${next.command.name}`
|
|
2344
|
-
: `${next.prompt.slice(0, 150)}${next.prompt.length > 150 ? '...' : ''}`;
|
|
2345
|
-
if (displayText.trim()) {
|
|
2346
|
-
await sendThreadMessage(this.thread, `${QUEUE_PREFIX}**${next.username}:** ${displayText}`);
|
|
2347
|
-
}
|
|
2348
|
+
await this.sendQueueDrainIndicator(next);
|
|
2348
2349
|
this.markQuestionQueueHandoffStarted(sessionId);
|
|
2349
2350
|
await this.submitViaOpencodeQueue(next);
|
|
2350
2351
|
}
|
|
@@ -2381,7 +2382,7 @@ export class ThreadSessionRuntime {
|
|
|
2381
2382
|
const secs = remainingSec % 60;
|
|
2382
2383
|
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
2383
2384
|
})();
|
|
2384
|
-
const chunk = `${
|
|
2385
|
+
const chunk = asSubtext(`${message} - retrying in ${duration} (attempt #${attempt})`);
|
|
2385
2386
|
const retryResult = await this.thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
2386
2387
|
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }));
|
|
2387
2388
|
if (retryResult instanceof Error) {
|
|
@@ -2447,7 +2448,7 @@ export class ThreadSessionRuntime {
|
|
|
2447
2448
|
const titlePrefix = properties.title
|
|
2448
2449
|
? `${properties.title.trim()}: `
|
|
2449
2450
|
: '';
|
|
2450
|
-
const chunk = `${
|
|
2451
|
+
const chunk = asSubtext(`${properties.variant}: ${titlePrefix}${toastMessage}`);
|
|
2451
2452
|
const toastResult = await this.thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
2452
2453
|
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }));
|
|
2453
2454
|
if (toastResult instanceof Error) {
|
|
@@ -2674,6 +2675,16 @@ export class ThreadSessionRuntime {
|
|
|
2674
2675
|
}
|
|
2675
2676
|
return fetched.topic?.trim() || undefined;
|
|
2676
2677
|
})();
|
|
2678
|
+
const system = await this.resolveTurnSystemPrompt({
|
|
2679
|
+
sessionId: session.id,
|
|
2680
|
+
channelTopic,
|
|
2681
|
+
agents: availableAgents,
|
|
2682
|
+
input,
|
|
2683
|
+
});
|
|
2684
|
+
if (system instanceof Error) {
|
|
2685
|
+
await cleanupOnError(`✗ Failed to prepare system prompt: ${system.message}`);
|
|
2686
|
+
return;
|
|
2687
|
+
}
|
|
2677
2688
|
const worktreeChanged = this.consumeWorktreePromptChange(worktree);
|
|
2678
2689
|
const syntheticContext = getOpencodePromptContext({
|
|
2679
2690
|
sessionId: session.id,
|
|
@@ -2687,6 +2698,8 @@ export class ThreadSessionRuntime {
|
|
|
2687
2698
|
worktree,
|
|
2688
2699
|
currentAgent: resolvedAgent,
|
|
2689
2700
|
worktreeChanged,
|
|
2701
|
+
systemPromptFromSourceSession: !isSystemPromptForSession({ system, sessionId: session.id }),
|
|
2702
|
+
parentSessionId: this.getParentSessionIdMissingFromSystem({ system, input }),
|
|
2690
2703
|
});
|
|
2691
2704
|
const parts = [
|
|
2692
2705
|
{ type: 'text', text: promptWithImagePaths },
|
|
@@ -2697,18 +2710,7 @@ export class ThreadSessionRuntime {
|
|
|
2697
2710
|
sessionID: session.id,
|
|
2698
2711
|
directory: this.sdkDirectory,
|
|
2699
2712
|
parts,
|
|
2700
|
-
system
|
|
2701
|
-
sessionId: session.id,
|
|
2702
|
-
channelId,
|
|
2703
|
-
guildId: this.thread.guildId,
|
|
2704
|
-
threadId: this.thread.id,
|
|
2705
|
-
channelTopic,
|
|
2706
|
-
agents: availableAgents,
|
|
2707
|
-
username: this.state?.sessionUsername || input.username,
|
|
2708
|
-
userId: this.state?.sessionUserId || input.userId,
|
|
2709
|
-
parentSessionId: this.state?.parentSessionId || input.parentSessionId,
|
|
2710
|
-
scheduledTask: await this.resolveScheduledTaskContext(session.id),
|
|
2711
|
-
}),
|
|
2713
|
+
system,
|
|
2712
2714
|
...(resolvedAgent ? { agent: resolvedAgent } : {}),
|
|
2713
2715
|
...(modelField ? { model: modelField } : {}),
|
|
2714
2716
|
...variantField,
|
|
@@ -2782,6 +2784,7 @@ export class ThreadSessionRuntime {
|
|
|
2782
2784
|
const queuedMessage = {
|
|
2783
2785
|
queueId,
|
|
2784
2786
|
prompt: input.prompt,
|
|
2787
|
+
queuedAction: input.queuedAction,
|
|
2785
2788
|
userId: input.userId,
|
|
2786
2789
|
username: input.username,
|
|
2787
2790
|
images: input.images,
|
|
@@ -2795,6 +2798,7 @@ export class ThreadSessionRuntime {
|
|
|
2795
2798
|
parentSessionId: input.parentSessionId,
|
|
2796
2799
|
sourceMessageId: input.sourceMessageId,
|
|
2797
2800
|
sourceThreadId: input.sourceThreadId,
|
|
2801
|
+
sourceChannelId: input.sourceChannelId,
|
|
2798
2802
|
repliedMessage: input.repliedMessage,
|
|
2799
2803
|
sessionStartScheduleKind: input.sessionStartSource?.scheduleKind,
|
|
2800
2804
|
sessionStartScheduledTaskId: input.sessionStartSource?.scheduledTaskId,
|
|
@@ -2802,10 +2806,25 @@ export class ThreadSessionRuntime {
|
|
|
2802
2806
|
};
|
|
2803
2807
|
let result = { queued: false, queueId };
|
|
2804
2808
|
await this.dispatchAction(async () => {
|
|
2809
|
+
// Determine if the message will genuinely wait in queue
|
|
2810
|
+
const position = (this.state?.queueItems.length ?? 0) + 1;
|
|
2811
|
+
result = this.isBusy()
|
|
2812
|
+
? { queued: true, position, queueId }
|
|
2813
|
+
: { queued: false, queueId };
|
|
2814
|
+
// Post the ack before the item is visible to any drain path.
|
|
2815
|
+
const queueAckMessageId = result.queued && input.onLocalQueued
|
|
2816
|
+
? await input.onLocalQueued({ queueId, position }).catch((error) => {
|
|
2817
|
+
logger.error(`[QUEUE] Failed to post queue ack for ${queueId}:`, error);
|
|
2818
|
+
return undefined;
|
|
2819
|
+
})
|
|
2820
|
+
: undefined;
|
|
2821
|
+
const item = queueAckMessageId
|
|
2822
|
+
? { ...queuedMessage, queueAckMessageId }
|
|
2823
|
+
: queuedMessage;
|
|
2805
2824
|
const persistResult = await insertThreadQueueItem({
|
|
2806
2825
|
queueId,
|
|
2807
2826
|
threadId: this.threadId,
|
|
2808
|
-
payloadJson: JSON.stringify(
|
|
2827
|
+
payloadJson: JSON.stringify(item),
|
|
2809
2828
|
}).catch((error) => {
|
|
2810
2829
|
return new Error('Failed to persist queued message', { cause: error });
|
|
2811
2830
|
});
|
|
@@ -2813,17 +2832,8 @@ export class ThreadSessionRuntime {
|
|
|
2813
2832
|
logger.error(`[QUEUE] Failed to persist queued message ${queueId} in thread ${this.threadId}: ${persistResult.message}`);
|
|
2814
2833
|
throw persistResult;
|
|
2815
2834
|
}
|
|
2816
|
-
threadState.enqueueItem(this.threadId,
|
|
2817
|
-
// Determine if the message is genuinely waiting in queue
|
|
2835
|
+
threadState.enqueueItem(this.threadId, item);
|
|
2818
2836
|
const stateAfterEnqueue = threadState.getThreadState(this.threadId);
|
|
2819
|
-
const position = stateAfterEnqueue?.queueItems.length ?? 0;
|
|
2820
|
-
const willDrainNow = stateAfterEnqueue
|
|
2821
|
-
? (stateAfterEnqueue.queueItems.length > 0
|
|
2822
|
-
&& !this.isBusy())
|
|
2823
|
-
: false;
|
|
2824
|
-
result = !willDrainNow && position > 0
|
|
2825
|
-
? { queued: true, position, queueId }
|
|
2826
|
-
: { queued: false, queueId };
|
|
2827
2837
|
if (this.hasPendingQuestionUi()) {
|
|
2828
2838
|
this.maybeHandoffQueuedItemForPendingQuestion({
|
|
2829
2839
|
sessionId: stateAfterEnqueue?.sessionId || this.state?.sessionId,
|
|
@@ -2934,6 +2944,7 @@ export class ThreadSessionRuntime {
|
|
|
2934
2944
|
const resolvedInput = maybeConvertLeadingCommand({
|
|
2935
2945
|
...input,
|
|
2936
2946
|
prompt: result.prompt,
|
|
2947
|
+
queuedAction: result.queuedAction,
|
|
2937
2948
|
images: result.images,
|
|
2938
2949
|
mode: result.mode,
|
|
2939
2950
|
// Voice transcription can extract an agent name — apply it only if
|
|
@@ -3022,17 +3033,20 @@ export class ThreadSessionRuntime {
|
|
|
3022
3033
|
apiAbortPromise,
|
|
3023
3034
|
};
|
|
3024
3035
|
}
|
|
3025
|
-
|
|
3036
|
+
/** Explicit user abort: stops the run and drops all queued messages. Returns the removed items. */
|
|
3037
|
+
async abortActiveRun(reason) {
|
|
3026
3038
|
const outcome = this.abortActiveRunInternal({
|
|
3027
3039
|
reason,
|
|
3028
3040
|
});
|
|
3029
3041
|
if (outcome.apiAbortPromise) {
|
|
3030
3042
|
void outcome.apiAbortPromise;
|
|
3031
3043
|
}
|
|
3032
|
-
//
|
|
3033
|
-
|
|
3034
|
-
|
|
3044
|
+
// Enqueued synchronously, so it runs before the abort's session.idle can drain the queue.
|
|
3045
|
+
let cleared = [];
|
|
3046
|
+
await this.dispatchAction(async () => {
|
|
3047
|
+
cleared = await this.clearQueueNow();
|
|
3035
3048
|
});
|
|
3049
|
+
return cleared;
|
|
3036
3050
|
}
|
|
3037
3051
|
async abortActiveRunAndWait({ reason, timeoutMs = 2_000, }) {
|
|
3038
3052
|
const state = this.state;
|
|
@@ -3073,17 +3087,21 @@ export class ThreadSessionRuntime {
|
|
|
3073
3087
|
async clearQueue() {
|
|
3074
3088
|
let cleared = [];
|
|
3075
3089
|
await this.dispatchAction(async () => {
|
|
3076
|
-
|
|
3077
|
-
return new Error('Failed to clear persisted queue', { cause: error });
|
|
3078
|
-
});
|
|
3079
|
-
if (persistResult instanceof Error) {
|
|
3080
|
-
logger.error(`[QUEUE] Failed to clear persisted queue for thread ${this.threadId}: ${persistResult.message}`);
|
|
3081
|
-
return;
|
|
3082
|
-
}
|
|
3083
|
-
cleared = threadState.clearQueueItems(this.threadId);
|
|
3090
|
+
cleared = await this.clearQueueNow();
|
|
3084
3091
|
});
|
|
3085
3092
|
return cleared;
|
|
3086
3093
|
}
|
|
3094
|
+
// Must run inside dispatchAction.
|
|
3095
|
+
async clearQueueNow() {
|
|
3096
|
+
const persistResult = await deleteThreadQueueItems(this.threadId).catch((error) => {
|
|
3097
|
+
return new Error('Failed to clear persisted queue', { cause: error });
|
|
3098
|
+
});
|
|
3099
|
+
if (persistResult instanceof Error) {
|
|
3100
|
+
logger.error(`[QUEUE] Failed to clear persisted queue for thread ${this.threadId}: ${persistResult.message}`);
|
|
3101
|
+
return [];
|
|
3102
|
+
}
|
|
3103
|
+
return threadState.clearQueueItems(this.threadId);
|
|
3104
|
+
}
|
|
3087
3105
|
/** Remove a queued message by its 1-based position. */
|
|
3088
3106
|
async removeQueuePosition(position) {
|
|
3089
3107
|
let removed;
|
|
@@ -3135,7 +3153,7 @@ export class ThreadSessionRuntime {
|
|
|
3135
3153
|
* Returns { found: true, removed } if the item was in the queue,
|
|
3136
3154
|
* or { found: false } if it was already dispatched or never queued.
|
|
3137
3155
|
*/
|
|
3138
|
-
async updateQueuedMessage(sourceMessageId, newPrompt) {
|
|
3156
|
+
async updateQueuedMessage({ sourceMessageId, newPrompt, queuedAction, }) {
|
|
3139
3157
|
let result = { found: false, removed: false };
|
|
3140
3158
|
await this.dispatchAction(async () => {
|
|
3141
3159
|
const trimmed = newPrompt.trim();
|
|
@@ -3151,7 +3169,7 @@ export class ThreadSessionRuntime {
|
|
|
3151
3169
|
const persistResult = trimmed
|
|
3152
3170
|
? await updateThreadQueueItemPayload({
|
|
3153
3171
|
queueId,
|
|
3154
|
-
payloadJson: JSON.stringify({ ...original, prompt: trimmed }),
|
|
3172
|
+
payloadJson: JSON.stringify({ ...original, prompt: trimmed, queuedAction }),
|
|
3155
3173
|
}).catch((error) => {
|
|
3156
3174
|
return new Error('Failed to update persisted queue item', { cause: error });
|
|
3157
3175
|
})
|
|
@@ -3167,7 +3185,7 @@ export class ThreadSessionRuntime {
|
|
|
3167
3185
|
threadState.updateQueueItemBySourceMessageId(this.threadId, sourceMessageId, (item) => {
|
|
3168
3186
|
if (!trimmed)
|
|
3169
3187
|
return null;
|
|
3170
|
-
return { ...item, prompt: trimmed };
|
|
3188
|
+
return { ...item, prompt: trimmed, queuedAction };
|
|
3171
3189
|
});
|
|
3172
3190
|
result = trimmed
|
|
3173
3191
|
? { found: true, removed: false }
|
|
@@ -3254,6 +3272,32 @@ export class ThreadSessionRuntime {
|
|
|
3254
3272
|
threadState.removeQueueItemById(this.threadId, item.queueId);
|
|
3255
3273
|
});
|
|
3256
3274
|
}
|
|
3275
|
+
// Silent reply to the message that queued the item. Discord replies must
|
|
3276
|
+
// reference a message in the same channel.
|
|
3277
|
+
async sendQueueDrainIndicator(item) {
|
|
3278
|
+
const replyTarget = item.queueAckMessageId
|
|
3279
|
+
?? (item.sourceChannelId === this.threadId ? item.sourceMessageId : undefined);
|
|
3280
|
+
const content = (() => {
|
|
3281
|
+
if (replyTarget) {
|
|
3282
|
+
return asSubtext('Executing queued prompt');
|
|
3283
|
+
}
|
|
3284
|
+
const preview = item.command
|
|
3285
|
+
? `/${item.command.name}`
|
|
3286
|
+
: item.prompt.replace(/\s+/g, ' ').trim().slice(0, 150);
|
|
3287
|
+
return asSubtext(`Executing queued prompt from ${item.username}: ${preview}`);
|
|
3288
|
+
})();
|
|
3289
|
+
const sendResult = await this.thread.send({
|
|
3290
|
+
content,
|
|
3291
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
3292
|
+
allowedMentions: { parse: [], repliedUser: false },
|
|
3293
|
+
reply: replyTarget
|
|
3294
|
+
? { messageReference: replyTarget, failIfNotExists: false }
|
|
3295
|
+
: undefined,
|
|
3296
|
+
}).catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }));
|
|
3297
|
+
if (sendResult instanceof Error) {
|
|
3298
|
+
discordLogger.error('Failed to send queue drain indicator:', sendResult);
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3257
3301
|
// ── Queue Drain ─────────────────────────────────────────────
|
|
3258
3302
|
/**
|
|
3259
3303
|
* Check if we can dispatch the next queued message. If so, dequeue and
|
|
@@ -3283,7 +3327,9 @@ export class ThreadSessionRuntime {
|
|
|
3283
3327
|
if (sessionBusy) {
|
|
3284
3328
|
return;
|
|
3285
3329
|
}
|
|
3286
|
-
|
|
3330
|
+
if (this.dispatchingQueueId)
|
|
3331
|
+
return;
|
|
3332
|
+
const next = thread.queueItems[0];
|
|
3287
3333
|
if (!next) {
|
|
3288
3334
|
return;
|
|
3289
3335
|
}
|
|
@@ -3292,12 +3338,7 @@ export class ThreadSessionRuntime {
|
|
|
3292
3338
|
// Show queued message indicator only for messages that actually waited
|
|
3293
3339
|
// behind a running request — not for the first immediate dispatch.
|
|
3294
3340
|
if (showIndicator) {
|
|
3295
|
-
|
|
3296
|
-
? `/${next.command.name}`
|
|
3297
|
-
: `${next.prompt.slice(0, 150)}${next.prompt.length > 150 ? '...' : ''}`;
|
|
3298
|
-
if (displayText.trim()) {
|
|
3299
|
-
await sendThreadMessage(this.thread, `${QUEUE_PREFIX}**${next.username}:** ${displayText}`);
|
|
3300
|
-
}
|
|
3341
|
+
await this.sendQueueDrainIndicator(next);
|
|
3301
3342
|
}
|
|
3302
3343
|
// Start dispatch (detached — does not block the action queue).
|
|
3303
3344
|
// The prompt call is long-running. Events continue to flow through
|
|
@@ -3305,7 +3346,7 @@ export class ThreadSessionRuntime {
|
|
|
3305
3346
|
// gating prevents concurrent local-queue dispatches. Mark busy now to
|
|
3306
3347
|
// close the tiny window before the first session.status busy arrives.
|
|
3307
3348
|
const dispatchSessionId = thread.sessionId;
|
|
3308
|
-
if (dispatchSessionId) {
|
|
3349
|
+
if (dispatchSessionId && next.queuedAction !== 'btw') {
|
|
3309
3350
|
this.markQueueDispatchBusy(dispatchSessionId);
|
|
3310
3351
|
}
|
|
3311
3352
|
let accepted = false;
|
|
@@ -3335,6 +3376,27 @@ export class ThreadSessionRuntime {
|
|
|
3335
3376
|
// The listener is already running, so this only handles
|
|
3336
3377
|
// session ensure + model/agent + SDK call + state.
|
|
3337
3378
|
async dispatchPrompt(input) {
|
|
3379
|
+
if (input.queuedAction === 'btw') {
|
|
3380
|
+
const { forkSessionToBtwThread } = await import('../commands/btw.js');
|
|
3381
|
+
const result = await forkSessionToBtwThread({
|
|
3382
|
+
sourceThread: this.thread,
|
|
3383
|
+
projectDirectory: this.projectDirectory,
|
|
3384
|
+
sdkDirectory: this.sdkDirectory,
|
|
3385
|
+
prompt: input.prompt,
|
|
3386
|
+
userId: input.userId,
|
|
3387
|
+
username: input.username,
|
|
3388
|
+
appId: input.appId,
|
|
3389
|
+
images: input.images,
|
|
3390
|
+
agent: input.agent,
|
|
3391
|
+
});
|
|
3392
|
+
if (result instanceof Error) {
|
|
3393
|
+
logger.error('[QUEUE] Could not fork queued btw:', result);
|
|
3394
|
+
await sendThreadMessage(this.thread, `Could not fork queued btw: ${result.message}`);
|
|
3395
|
+
return true;
|
|
3396
|
+
}
|
|
3397
|
+
await sendThreadMessage(this.thread, `Session forked! Continue in ${result.thread.toString()}`);
|
|
3398
|
+
return true;
|
|
3399
|
+
}
|
|
3338
3400
|
this.lastDisplayedContextPercentage = 0;
|
|
3339
3401
|
this.lastRateLimitDisplayTime = 0;
|
|
3340
3402
|
this.lastSentPartKind = undefined;
|
|
@@ -3530,6 +3592,23 @@ export class ThreadSessionRuntime {
|
|
|
3530
3592
|
}
|
|
3531
3593
|
return fetched.topic?.trim() || undefined;
|
|
3532
3594
|
})();
|
|
3595
|
+
// Pinned before building parts so the fork notice can compare identities.
|
|
3596
|
+
// Also covers session.command: the context-awareness plugin reads the same
|
|
3597
|
+
// pinned file because the command API has no system field.
|
|
3598
|
+
const system = await this.resolveTurnSystemPrompt({
|
|
3599
|
+
sessionId: session.id,
|
|
3600
|
+
channelTopic,
|
|
3601
|
+
agents: earlyAvailableAgents,
|
|
3602
|
+
input,
|
|
3603
|
+
});
|
|
3604
|
+
if (system instanceof Error) {
|
|
3605
|
+
logger.error(`[DISPATCH] Failed to pin system prompt for session ${session.id}: ${system.message}`);
|
|
3606
|
+
void notifyError(system, 'Failed to pin session system prompt');
|
|
3607
|
+
this.stopTyping();
|
|
3608
|
+
await sendThreadMessage(this.thread, `✗ Failed to prepare system prompt: ${system.message}`, { flags: NOTIFY_MESSAGE_FLAGS });
|
|
3609
|
+
return false;
|
|
3610
|
+
}
|
|
3611
|
+
const systemPromptFromSourceSession = !isSystemPromptForSession({ system, sessionId: session.id });
|
|
3533
3612
|
const worktreeChanged = this.consumeWorktreePromptChange(worktree);
|
|
3534
3613
|
const syntheticContext = getOpencodePromptContext({
|
|
3535
3614
|
sessionId: session.id,
|
|
@@ -3543,6 +3622,8 @@ export class ThreadSessionRuntime {
|
|
|
3543
3622
|
worktree,
|
|
3544
3623
|
currentAgent: earlyAgentPreference,
|
|
3545
3624
|
worktreeChanged,
|
|
3625
|
+
systemPromptFromSourceSession,
|
|
3626
|
+
parentSessionId: this.getParentSessionIdMissingFromSystem({ system, input }),
|
|
3546
3627
|
});
|
|
3547
3628
|
const parts = [
|
|
3548
3629
|
{ type: 'text', text: promptWithImagePaths },
|
|
@@ -3586,40 +3667,9 @@ export class ThreadSessionRuntime {
|
|
|
3586
3667
|
sourceThreadId: input.sourceThreadId || this.thread.id,
|
|
3587
3668
|
threadName: this.thread.name || undefined,
|
|
3588
3669
|
repliedMessage: input.repliedMessage,
|
|
3670
|
+
systemPromptFromSourceSession,
|
|
3671
|
+
parentSessionId: this.getParentSessionIdMissingFromSystem({ system, input }),
|
|
3589
3672
|
});
|
|
3590
|
-
// OpenCode's session.command API has no `system` field. Persist the
|
|
3591
|
-
// kimaki system prompt so the context-awareness plugin can attach it
|
|
3592
|
-
// on chat.message when the command user message is created.
|
|
3593
|
-
// Fail the dispatch if persistence fails — otherwise the command runs
|
|
3594
|
-
// without Discord context and silently restores the original bug.
|
|
3595
|
-
const commandSystem = getOpencodeSystemMessage({
|
|
3596
|
-
sessionId: session.id,
|
|
3597
|
-
channelId,
|
|
3598
|
-
guildId: this.thread.guildId,
|
|
3599
|
-
threadId: this.thread.id,
|
|
3600
|
-
channelTopic,
|
|
3601
|
-
agents: earlyAvailableAgents,
|
|
3602
|
-
username: this.state?.sessionUsername || input.username,
|
|
3603
|
-
userId: this.state?.sessionUserId || input.userId,
|
|
3604
|
-
parentSessionId: this.state?.parentSessionId || input.parentSessionId,
|
|
3605
|
-
scheduledTask: await this.resolveScheduledTaskContext(session.id),
|
|
3606
|
-
});
|
|
3607
|
-
const systemWriteResult = await writeSessionSystemPrompt({
|
|
3608
|
-
sessionId: session.id,
|
|
3609
|
-
system: commandSystem,
|
|
3610
|
-
dataDir: getDataDir(),
|
|
3611
|
-
}).catch((e) => {
|
|
3612
|
-
return e instanceof Error
|
|
3613
|
-
? e
|
|
3614
|
-
: new Error(String(e), { cause: e });
|
|
3615
|
-
});
|
|
3616
|
-
if (systemWriteResult instanceof Error) {
|
|
3617
|
-
logger.error(`[DISPATCH] Failed to persist system prompt for command session ${session.id}: ${systemWriteResult.message}`);
|
|
3618
|
-
void notifyError(systemWriteResult, 'Failed to persist system prompt before session.command');
|
|
3619
|
-
this.stopTyping();
|
|
3620
|
-
await sendThreadMessage(this.thread, `✗ Failed to prepare command system prompt: ${systemWriteResult.message}`, { flags: NOTIFY_MESSAGE_FLAGS });
|
|
3621
|
-
return false;
|
|
3622
|
-
}
|
|
3623
3673
|
const commandResponse = await getClient().session.command({
|
|
3624
3674
|
sessionID: session.id,
|
|
3625
3675
|
directory: this.sdkDirectory,
|
|
@@ -3682,18 +3732,7 @@ export class ThreadSessionRuntime {
|
|
|
3682
3732
|
sessionID: session.id,
|
|
3683
3733
|
directory: this.sdkDirectory,
|
|
3684
3734
|
parts,
|
|
3685
|
-
system
|
|
3686
|
-
sessionId: session.id,
|
|
3687
|
-
channelId,
|
|
3688
|
-
guildId: this.thread.guildId,
|
|
3689
|
-
threadId: this.thread.id,
|
|
3690
|
-
channelTopic,
|
|
3691
|
-
agents: earlyAvailableAgents,
|
|
3692
|
-
username: this.state?.sessionUsername || input.username,
|
|
3693
|
-
userId: this.state?.sessionUserId || input.userId,
|
|
3694
|
-
parentSessionId: this.state?.parentSessionId || input.parentSessionId,
|
|
3695
|
-
scheduledTask: await this.resolveScheduledTaskContext(session.id),
|
|
3696
|
-
}),
|
|
3735
|
+
system,
|
|
3697
3736
|
model: earlyModelParam,
|
|
3698
3737
|
agent: earlyAgentPreference,
|
|
3699
3738
|
...variantField,
|
|
@@ -3761,6 +3800,38 @@ export class ThreadSessionRuntime {
|
|
|
3761
3800
|
cache.set(sessionId, context);
|
|
3762
3801
|
return context;
|
|
3763
3802
|
}
|
|
3803
|
+
/**
|
|
3804
|
+
* Pinned system prompt for this turn. Generated only on the first turn of a
|
|
3805
|
+
* session; forks start with the source session's pinned prompt.
|
|
3806
|
+
*/
|
|
3807
|
+
async resolveTurnSystemPrompt({ sessionId, channelTopic, agents, input, }) {
|
|
3808
|
+
return resolveSessionSystemPrompt({
|
|
3809
|
+
sessionId,
|
|
3810
|
+
generate: async () => {
|
|
3811
|
+
return getOpencodeSystemMessage({
|
|
3812
|
+
sessionId,
|
|
3813
|
+
channelId: this.channelId,
|
|
3814
|
+
guildId: this.thread.guildId,
|
|
3815
|
+
threadId: this.thread.id,
|
|
3816
|
+
channelTopic,
|
|
3817
|
+
agents,
|
|
3818
|
+
username: this.state?.sessionUsername || input.username,
|
|
3819
|
+
userId: this.state?.sessionUserId || input.userId,
|
|
3820
|
+
parentSessionId: this.state?.parentSessionId || input.parentSessionId,
|
|
3821
|
+
scheduledTask: await this.resolveScheduledTaskContext(sessionId),
|
|
3822
|
+
});
|
|
3823
|
+
},
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3826
|
+
/** Parent set after the first turn is not in the pinned prompt; send it per turn. */
|
|
3827
|
+
getParentSessionIdMissingFromSystem({ system, input, }) {
|
|
3828
|
+
const parentSessionId = this.state?.parentSessionId || input.parentSessionId;
|
|
3829
|
+
if (!parentSessionId)
|
|
3830
|
+
return undefined;
|
|
3831
|
+
if (systemPromptHasParentSession({ system, parentSessionId }))
|
|
3832
|
+
return undefined;
|
|
3833
|
+
return parentSessionId;
|
|
3834
|
+
}
|
|
3764
3835
|
async updateExistingSessionPermissions({ client, sessionId, createdNewSession, permissions, }) {
|
|
3765
3836
|
if (createdNewSession) {
|
|
3766
3837
|
return null;
|
|
@@ -3912,7 +3983,7 @@ export class ThreadSessionRuntime {
|
|
|
3912
3983
|
const agentLabel = agent && agent.toLowerCase() !== 'build'
|
|
3913
3984
|
? ` ⋅ ${agent}`
|
|
3914
3985
|
: '';
|
|
3915
|
-
const result = await sendThreadMessage(this.thread,
|
|
3986
|
+
const result = await sendThreadMessage(this.thread, asSubtext(`*using ${modelLabel}${agentLabel}*`), { flags: SILENT_MESSAGE_FLAGS }).catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }));
|
|
3916
3987
|
if (result instanceof Error) {
|
|
3917
3988
|
logger.warn(`[SESSION INFO] Failed to send model info: ${result.message}`);
|
|
3918
3989
|
}
|
|
@@ -4041,7 +4112,7 @@ export class ThreadSessionRuntime {
|
|
|
4041
4112
|
})
|
|
4042
4113
|
: undefined;
|
|
4043
4114
|
const mention = mentionUserId ? ` <@${mentionUserId}>` : '';
|
|
4044
|
-
const footerText =
|
|
4115
|
+
const footerText = asSubtext(`*${projectInfo}${sessionDuration}${contextInfo}${modelInfo}${agentInfo}*${mention}`);
|
|
4045
4116
|
this.stopTyping();
|
|
4046
4117
|
await sendThreadMessage(this.thread, footerText, {
|
|
4047
4118
|
flags: shouldNotifyUser ? NOTIFY_MESSAGE_FLAGS : SILENT_MESSAGE_FLAGS,
|
|
@@ -4057,6 +4128,16 @@ export class ThreadSessionRuntime {
|
|
|
4057
4128
|
this.lastSentPartKind = undefined;
|
|
4058
4129
|
}
|
|
4059
4130
|
async maybeNotifyPromptCacheClear({ sessionId, messageId, }) {
|
|
4131
|
+
// Only the first reply after a user prompt can show a cold cache. Later steps re-read the turn's own writes.
|
|
4132
|
+
// TODO: send this before step 1 tools finish. Tokens arrive only with step-finish, after tools in v1 and v2.
|
|
4133
|
+
// OpenCode v2 publishes `session.step.streamed` at stream end (before tools settle) but without tokens, and
|
|
4134
|
+
// packages/ai drops Anthropic `message_start` usage (onMessageStart returns NO_EVENTS). Needs upstream:
|
|
4135
|
+
// add tokens to Step.Streamed, or emit a usage event on message_start for a notice right after the prompt.
|
|
4136
|
+
// https://github.com/anomalyco/opencode/blob/v2/packages/schema/src/session-event.ts
|
|
4137
|
+
const [firstAssistantId] = this.getAssistantMessageIdsForCurrentTurn({ sessionId });
|
|
4138
|
+
if (firstAssistantId !== messageId) {
|
|
4139
|
+
return;
|
|
4140
|
+
}
|
|
4060
4141
|
const cacheClear = getPromptCacheClear({
|
|
4061
4142
|
events: this.eventBuffer,
|
|
4062
4143
|
sessionId,
|
|
@@ -4070,7 +4151,7 @@ export class ThreadSessionRuntime {
|
|
|
4070
4151
|
previousMessageId: cacheClear.previousMessageId,
|
|
4071
4152
|
currentMessageId: cacheClear.currentMessageId,
|
|
4072
4153
|
});
|
|
4073
|
-
const chunk =
|
|
4154
|
+
const chunk = asSubtext(formatPromptCacheClearMessage(cacheClear, systemDiff));
|
|
4074
4155
|
const sendResult = await this.thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
4075
4156
|
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }));
|
|
4076
4157
|
if (sendResult instanceof Error) {
|
package/dist/sleep.e2e.test.js
CHANGED
|
@@ -313,15 +313,15 @@ describe('kimaki_sleep', () => {
|
|
|
313
313
|
-# *using deterministic-provider/deterministic-v2*
|
|
314
314
|
> waiting for the deploy
|
|
315
315
|
|
|
316
|
-
┣ kimaki_sleep until 2030-01-01T09:00:00Z _waiting for the deploy_
|
|
316
|
+
-# ┣ kimaki_sleep until 2030-01-01T09:00:00Z _waiting for the deploy_
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
sleep-started
|
|
319
319
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
320
320
|
-# Woke after sleeping until 2030-01-01 09:00 UTC
|
|
321
321
|
Reason: waiting for the deploy
|
|
322
322
|
Continue the work you were waiting for.
|
|
323
323
|
[embed]
|
|
324
|
-
|
|
324
|
+
sleep-wake-done
|
|
325
325
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
326
326
|
`);
|
|
327
327
|
// The wake must continue the SAME OpenCode session, not start a new one:
|
|
@@ -412,14 +412,14 @@ describe('kimaki_sleep', () => {
|
|
|
412
412
|
SLEEP_CANCEL_MARKER wait for something
|
|
413
413
|
--- from: assistant (TestBot)
|
|
414
414
|
-# *using deterministic-provider/deterministic-v2*
|
|
415
|
-
┣ kimaki_sleep for 2h _cancel me_
|
|
415
|
+
-# ┣ kimaki_sleep for 2h _cancel me_
|
|
416
416
|
|
|
417
|
-
|
|
417
|
+
cancel-sleep-started
|
|
418
418
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
419
419
|
--- from: user (sleep-tester)
|
|
420
420
|
SLEEP_CANCEL_FOLLOWUP never mind, keep going
|
|
421
421
|
--- from: assistant (TestBot)
|
|
422
|
-
|
|
422
|
+
cancel-followup-done
|
|
423
423
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
424
424
|
`);
|
|
425
425
|
}, 20_000);
|