kimaki 0.30.0 → 0.30.1
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/cli-commands/send.js +3 -9
- package/dist/commands/abort.js +16 -4
- package/dist/commands/queue.js +7 -7
- package/dist/discord-bot.js +5 -5
- package/dist/discord-utils.js +15 -1
- package/dist/external-directory-permissions.e2e.test.js +2 -2
- package/dist/external-directory-project-deny.e2e.test.js +1 -1
- package/dist/message-formatting.js +16 -6
- package/dist/message-formatting.test.js +10 -9
- 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 +61 -5
- package/dist/queue-advanced-footer.e2e.test.js +1 -1
- package/dist/queue-advanced-permissions-typing.e2e.test.js +1 -1
- package/dist/session-handler/event-stream-state.js +15 -15
- package/dist/session-handler/event-stream-state.test.js +152 -11
- package/dist/session-handler/thread-session-runtime.js +41 -28
- package/dist/sleep.e2e.test.js +2 -2
- package/dist/task-runner.js +3 -3
- package/dist/task-schedule.js +2 -2
- package/dist/thread-message-queue.e2e.test.js +1 -1
- package/dist/undo-redo.e2e.test.js +1 -1
- package/package.json +6 -6
- package/skills/holocron/SKILL.md +0 -28
- package/src/cli-commands/send.ts +3 -9
- package/src/commands/abort.ts +20 -4
- package/src/commands/queue.ts +7 -7
- package/src/discord-bot.ts +5 -5
- package/src/discord-utils.ts +17 -1
- package/src/external-directory-permissions.e2e.test.ts +2 -2
- package/src/external-directory-project-deny.e2e.test.ts +1 -1
- package/src/message-formatting.test.ts +10 -9
- package/src/message-formatting.ts +15 -6
- 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 +72 -5
- package/src/queue-advanced-footer.e2e.test.ts +1 -1
- package/src/queue-advanced-permissions-typing.e2e.test.ts +1 -1
- package/src/session-handler/event-stream-state.test.ts +155 -11
- package/src/session-handler/event-stream-state.ts +17 -20
- package/src/session-handler/thread-session-runtime.ts +44 -32
- package/src/sleep.e2e.test.ts +2 -2
- package/src/task-runner.ts +3 -3
- package/src/task-schedule.ts +2 -2
- package/src/thread-message-queue.e2e.test.ts +1 -1
- package/src/undo-redo.e2e.test.ts +1 -1
|
@@ -879,7 +879,8 @@ const MIN_PROMPT_CACHE_READ_TO_TRACK = 1024
|
|
|
879
879
|
const PROMPT_CACHE_DROP_RATIO = 0.5
|
|
880
880
|
|
|
881
881
|
export type PromptCacheClear = {
|
|
882
|
-
|
|
882
|
+
// Tokens that should have been read from cache: the smaller of the previous cached prefix and the current prompt.
|
|
883
|
+
expectedCacheRead: number
|
|
883
884
|
currentCacheRead: number
|
|
884
885
|
previousMessageId: string
|
|
885
886
|
currentMessageId: string
|
|
@@ -915,13 +916,6 @@ function hasPruneBetween({
|
|
|
915
916
|
return false
|
|
916
917
|
}
|
|
917
918
|
|
|
918
|
-
function promptInputTokens(tokens: {
|
|
919
|
-
input: number
|
|
920
|
-
cache: { read: number; write: number }
|
|
921
|
-
}): number {
|
|
922
|
-
return tokens.input + tokens.cache.read + tokens.cache.write
|
|
923
|
-
}
|
|
924
|
-
|
|
925
919
|
function getCompletedAssistantAt({
|
|
926
920
|
events,
|
|
927
921
|
sessionId,
|
|
@@ -963,7 +957,8 @@ function isComparableCacheAssistant(message: AssistantMessage): boolean {
|
|
|
963
957
|
return true
|
|
964
958
|
}
|
|
965
959
|
|
|
966
|
-
// Same-model cache drop vs the previous completed assistant.
|
|
960
|
+
// Same-model cache drop vs the previous completed assistant. Aborted/errored replies are skipped,
|
|
961
|
+
// compaction summaries and pruned tool output block the pair since they rewrite the prompt.
|
|
967
962
|
export function getPromptCacheClear({
|
|
968
963
|
events,
|
|
969
964
|
sessionId,
|
|
@@ -1003,13 +998,13 @@ export function getPromptCacheClear({
|
|
|
1003
998
|
continue
|
|
1004
999
|
}
|
|
1005
1000
|
seen.add(info.id)
|
|
1006
|
-
if (!
|
|
1001
|
+
if (!isUserFacingAssistantMessage(info)) {
|
|
1007
1002
|
return undefined
|
|
1008
1003
|
}
|
|
1009
|
-
if (info
|
|
1010
|
-
|
|
1004
|
+
if (!isComparableCacheAssistant(info) || !info.tokens) {
|
|
1005
|
+
continue
|
|
1011
1006
|
}
|
|
1012
|
-
if (info.
|
|
1007
|
+
if (info.modelID !== current.modelID || info.providerID !== current.providerID) {
|
|
1013
1008
|
return undefined
|
|
1014
1009
|
}
|
|
1015
1010
|
if (hasPruneBetween({
|
|
@@ -1020,17 +1015,19 @@ export function getPromptCacheClear({
|
|
|
1020
1015
|
})) {
|
|
1021
1016
|
return undefined
|
|
1022
1017
|
}
|
|
1023
|
-
|
|
1018
|
+
// Anthropic reports a fresh cache as write only, so read alone misses the turn after a miss.
|
|
1019
|
+
const previousCached = info.tokens.cache.read + info.tokens.cache.write
|
|
1020
|
+
const currentPrompt = current.tokens.input + current.tokens.cache.read + current.tokens.cache.write
|
|
1021
|
+
// A reverted (shorter) prompt can only reuse its own length from cache.
|
|
1022
|
+
const expectedCacheRead = Math.min(previousCached, currentPrompt)
|
|
1023
|
+
if (expectedCacheRead < MIN_PROMPT_CACHE_READ_TO_TRACK) {
|
|
1024
1024
|
return undefined
|
|
1025
1025
|
}
|
|
1026
|
-
|
|
1027
|
-
const currentPrompt = promptInputTokens(current.tokens)
|
|
1028
|
-
// Pruning blanks old tool output and shrinks the next prompt. That is not a cache clear.
|
|
1029
|
-
if (currentPrompt < previousPrompt) {
|
|
1026
|
+
if (current.tokens.cache.read > expectedCacheRead * PROMPT_CACHE_DROP_RATIO) {
|
|
1030
1027
|
return undefined
|
|
1031
1028
|
}
|
|
1032
1029
|
return {
|
|
1033
|
-
|
|
1030
|
+
expectedCacheRead,
|
|
1034
1031
|
currentCacheRead: current.tokens.cache.read,
|
|
1035
1032
|
previousMessageId: info.id,
|
|
1036
1033
|
currentMessageId: current.id,
|
|
@@ -1052,7 +1049,7 @@ export function formatPromptCacheClearMessage(
|
|
|
1052
1049
|
clear: PromptCacheClear,
|
|
1053
1050
|
systemDiff?: { additions: number; deletions: number },
|
|
1054
1051
|
): string {
|
|
1055
|
-
const tokens = `prompt cache missed (${formatCompactTokenCount(clear.
|
|
1052
|
+
const tokens = `prompt cache missed (${formatCompactTokenCount(clear.expectedCacheRead)} → ${formatCompactTokenCount(clear.currentCacheRead)})`
|
|
1056
1053
|
if (!systemDiff || (systemDiff.additions === 0 && systemDiff.deletions === 0)) {
|
|
1057
1054
|
return tokens
|
|
1058
1055
|
}
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
import type { DiscordFileAttachment, SessionPartKind } from '../message-formatting.js'
|
|
51
51
|
import {
|
|
52
52
|
asDiscordQuote,
|
|
53
|
-
|
|
53
|
+
asSubtext,
|
|
54
54
|
formatPart,
|
|
55
55
|
formatTaskToolTitle,
|
|
56
56
|
planAssistantTurnFlush,
|
|
@@ -2349,16 +2349,15 @@ export class ThreadSessionRuntime {
|
|
|
2349
2349
|
upToIndex: this.eventBuffer.length - 2,
|
|
2350
2350
|
})
|
|
2351
2351
|
const completedAt = msg.time.completed
|
|
2352
|
-
if (
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
})
|
|
2361
|
-
return
|
|
2352
|
+
if (!wasAlreadyCompleted && typeof completedAt === 'number') {
|
|
2353
|
+
if (isAssistantMessageNaturalCompletion({ message: msg })) {
|
|
2354
|
+
await this.handleNaturalAssistantCompletion({
|
|
2355
|
+
completedMessageId: msg.id,
|
|
2356
|
+
completedAt,
|
|
2357
|
+
})
|
|
2358
|
+
return
|
|
2359
|
+
}
|
|
2360
|
+
await this.maybeNotifyPromptCacheClear({ sessionId, messageId: msg.id })
|
|
2362
2361
|
}
|
|
2363
2362
|
|
|
2364
2363
|
// Context usage notice.
|
|
@@ -2398,7 +2397,7 @@ export class ThreadSessionRuntime {
|
|
|
2398
2397
|
return
|
|
2399
2398
|
}
|
|
2400
2399
|
this.lastDisplayedContextPercentage = thresholdCrossed
|
|
2401
|
-
const chunk =
|
|
2400
|
+
const chunk = asSubtext(`context usage ${currentPercentage}%`)
|
|
2402
2401
|
const sendResult = await this.thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
2403
2402
|
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }))
|
|
2404
2403
|
if (sendResult instanceof Error) {
|
|
@@ -2615,7 +2614,7 @@ export class ThreadSessionRuntime {
|
|
|
2615
2614
|
}
|
|
2616
2615
|
return ` (${pct.toFixed(1)}%)`
|
|
2617
2616
|
})()
|
|
2618
|
-
const chunk = `${STATUS_PREFIX}${part.tool} returned ${formattedTokens} tokens${percentageSuffix}`
|
|
2617
|
+
const chunk = asSubtext(`${STATUS_PREFIX}${part.tool} returned ${formattedTokens} tokens${percentageSuffix}`)
|
|
2619
2618
|
const largeOutputResult = await this.thread.send({
|
|
2620
2619
|
content: chunk,
|
|
2621
2620
|
flags: SILENT_MESSAGE_FLAGS,
|
|
@@ -3285,7 +3284,7 @@ export class ThreadSessionRuntime {
|
|
|
3285
3284
|
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`
|
|
3286
3285
|
})()
|
|
3287
3286
|
|
|
3288
|
-
const chunk = `${
|
|
3287
|
+
const chunk = asSubtext(`${message} - retrying in ${duration} (attempt #${attempt})`)
|
|
3289
3288
|
const retryResult = await this.thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
3290
3289
|
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }))
|
|
3291
3290
|
if (retryResult instanceof Error) {
|
|
@@ -3371,7 +3370,7 @@ export class ThreadSessionRuntime {
|
|
|
3371
3370
|
const titlePrefix = properties.title
|
|
3372
3371
|
? `${properties.title.trim()}: `
|
|
3373
3372
|
: ''
|
|
3374
|
-
const chunk = `${
|
|
3373
|
+
const chunk = asSubtext(`${properties.variant}: ${titlePrefix}${toastMessage}`)
|
|
3375
3374
|
const toastResult = await this.thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
3376
3375
|
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }))
|
|
3377
3376
|
if (toastResult instanceof Error) {
|
|
@@ -4051,17 +4050,20 @@ export class ThreadSessionRuntime {
|
|
|
4051
4050
|
}
|
|
4052
4051
|
}
|
|
4053
4052
|
|
|
4054
|
-
|
|
4053
|
+
/** Explicit user abort: stops the run and drops all queued messages. Returns the removed items. */
|
|
4054
|
+
async abortActiveRun(reason: string): Promise<threadState.QueuedMessage[]> {
|
|
4055
4055
|
const outcome = this.abortActiveRunInternal({
|
|
4056
4056
|
reason,
|
|
4057
4057
|
})
|
|
4058
4058
|
if (outcome.apiAbortPromise) {
|
|
4059
4059
|
void outcome.apiAbortPromise
|
|
4060
4060
|
}
|
|
4061
|
-
//
|
|
4062
|
-
|
|
4063
|
-
|
|
4061
|
+
// Enqueued synchronously, so it runs before the abort's session.idle can drain the queue.
|
|
4062
|
+
let cleared: threadState.QueuedMessage[] = []
|
|
4063
|
+
await this.dispatchAction(async () => {
|
|
4064
|
+
cleared = await this.clearQueueNow()
|
|
4064
4065
|
})
|
|
4066
|
+
return cleared
|
|
4065
4067
|
}
|
|
4066
4068
|
|
|
4067
4069
|
async abortActiveRunAndWait({
|
|
@@ -4112,20 +4114,25 @@ export class ThreadSessionRuntime {
|
|
|
4112
4114
|
async clearQueue(): Promise<threadState.QueuedMessage[]> {
|
|
4113
4115
|
let cleared: threadState.QueuedMessage[] = []
|
|
4114
4116
|
await this.dispatchAction(async () => {
|
|
4115
|
-
|
|
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)
|
|
4117
|
+
cleared = await this.clearQueueNow()
|
|
4125
4118
|
})
|
|
4126
4119
|
return cleared
|
|
4127
4120
|
}
|
|
4128
4121
|
|
|
4122
|
+
// Must run inside dispatchAction.
|
|
4123
|
+
private async clearQueueNow(): Promise<threadState.QueuedMessage[]> {
|
|
4124
|
+
const persistResult = await deleteThreadQueueItems(this.threadId).catch((error) => {
|
|
4125
|
+
return new Error('Failed to clear persisted queue', { cause: error })
|
|
4126
|
+
})
|
|
4127
|
+
if (persistResult instanceof Error) {
|
|
4128
|
+
logger.error(
|
|
4129
|
+
`[QUEUE] Failed to clear persisted queue for thread ${this.threadId}: ${persistResult.message}`,
|
|
4130
|
+
)
|
|
4131
|
+
return []
|
|
4132
|
+
}
|
|
4133
|
+
return threadState.clearQueueItems(this.threadId)
|
|
4134
|
+
}
|
|
4135
|
+
|
|
4129
4136
|
/** Remove a queued message by its 1-based position. */
|
|
4130
4137
|
async removeQueuePosition(position: number): Promise<threadState.QueuedMessage | undefined> {
|
|
4131
4138
|
let removed: threadState.QueuedMessage | undefined
|
|
@@ -5174,7 +5181,7 @@ export class ThreadSessionRuntime {
|
|
|
5174
5181
|
: ''
|
|
5175
5182
|
const result = await sendThreadMessage(
|
|
5176
5183
|
this.thread,
|
|
5177
|
-
|
|
5184
|
+
asSubtext(`*using ${modelLabel}${agentLabel}*`),
|
|
5178
5185
|
{ flags: SILENT_MESSAGE_FLAGS },
|
|
5179
5186
|
).catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }))
|
|
5180
5187
|
if (result instanceof Error) {
|
|
@@ -5327,7 +5334,7 @@ export class ThreadSessionRuntime {
|
|
|
5327
5334
|
})
|
|
5328
5335
|
: undefined
|
|
5329
5336
|
const mention = mentionUserId ? ` <@${mentionUserId}>` : ''
|
|
5330
|
-
const footerText =
|
|
5337
|
+
const footerText = asSubtext(
|
|
5331
5338
|
`*${projectInfo}${sessionDuration}${contextInfo}${modelInfo}${agentInfo}*${mention}`,
|
|
5332
5339
|
)
|
|
5333
5340
|
this.stopTyping()
|
|
@@ -5356,6 +5363,11 @@ export class ThreadSessionRuntime {
|
|
|
5356
5363
|
sessionId: string
|
|
5357
5364
|
messageId: string
|
|
5358
5365
|
}): Promise<void> {
|
|
5366
|
+
// Only the first reply after a user prompt can show a cold cache. Later steps re-read the turn's own writes.
|
|
5367
|
+
const [firstAssistantId] = this.getAssistantMessageIdsForCurrentTurn({ sessionId })
|
|
5368
|
+
if (firstAssistantId !== messageId) {
|
|
5369
|
+
return
|
|
5370
|
+
}
|
|
5359
5371
|
const cacheClear = getPromptCacheClear({
|
|
5360
5372
|
events: this.eventBuffer,
|
|
5361
5373
|
sessionId,
|
|
@@ -5369,7 +5381,7 @@ export class ThreadSessionRuntime {
|
|
|
5369
5381
|
previousMessageId: cacheClear.previousMessageId,
|
|
5370
5382
|
currentMessageId: cacheClear.currentMessageId,
|
|
5371
5383
|
})
|
|
5372
|
-
const chunk =
|
|
5384
|
+
const chunk = asSubtext(formatPromptCacheClearMessage(cacheClear, systemDiff))
|
|
5373
5385
|
const sendResult = await this.thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
5374
5386
|
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }))
|
|
5375
5387
|
if (sendResult instanceof Error) {
|
package/src/sleep.e2e.test.ts
CHANGED
|
@@ -347,7 +347,7 @@ describe('kimaki_sleep', () => {
|
|
|
347
347
|
-# *using deterministic-provider/deterministic-v2*
|
|
348
348
|
> waiting for the deploy
|
|
349
349
|
|
|
350
|
-
┣ kimaki_sleep until 2030-01-01T09:00:00Z _waiting for the deploy_
|
|
350
|
+
-# ┣ kimaki_sleep until 2030-01-01T09:00:00Z _waiting for the deploy_
|
|
351
351
|
|
|
352
352
|
> sleep-started
|
|
353
353
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
@@ -466,7 +466,7 @@ describe('kimaki_sleep', () => {
|
|
|
466
466
|
SLEEP_CANCEL_MARKER wait for something
|
|
467
467
|
--- from: assistant (TestBot)
|
|
468
468
|
-# *using deterministic-provider/deterministic-v2*
|
|
469
|
-
┣ kimaki_sleep for 2h _cancel me_
|
|
469
|
+
-# ┣ kimaki_sleep for 2h _cancel me_
|
|
470
470
|
|
|
471
471
|
> cancel-sleep-started
|
|
472
472
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
package/src/task-runner.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { DiscordAPIError, type REST, Routes } from 'discord.js'
|
|
4
4
|
import { createDiscordRest } from './discord-urls.js'
|
|
5
|
-
import { ensureThreadMember } from './discord-utils.js'
|
|
5
|
+
import { buildThreadStartEmbeds, ensureThreadMember } from './discord-utils.js'
|
|
6
6
|
import YAML from 'yaml'
|
|
7
7
|
import {
|
|
8
8
|
claimScheduledTaskRunning,
|
|
@@ -97,7 +97,7 @@ async function executeThreadScheduledTask({
|
|
|
97
97
|
: {}),
|
|
98
98
|
...(payload.parentSessionId ? { parentSessionId: payload.parentSessionId } : {}),
|
|
99
99
|
}
|
|
100
|
-
const embed =
|
|
100
|
+
const embed = await buildThreadStartEmbeds(marker)
|
|
101
101
|
// Newline between prefix and prompt so leading /command detection can
|
|
102
102
|
// find the command on its own line.
|
|
103
103
|
const prefixedPrompt = `${QUEUE_PREFIX}**kimaki-cli:**\n${prompt}`
|
|
@@ -296,7 +296,7 @@ async function executeChannelScheduledTask({
|
|
|
296
296
|
...(payload.parentSessionId ? { parentSessionId: payload.parentSessionId } : {}),
|
|
297
297
|
}
|
|
298
298
|
const embeds = marker
|
|
299
|
-
?
|
|
299
|
+
? await buildThreadStartEmbeds(marker)
|
|
300
300
|
: undefined
|
|
301
301
|
|
|
302
302
|
const starterResult = await rest
|
package/src/task-schedule.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { CronExpressionParser } from 'cron-parser'
|
|
4
4
|
import * as errore from 'errore'
|
|
5
|
-
import {
|
|
5
|
+
import { asSubtext } from './message-formatting.js'
|
|
6
6
|
|
|
7
7
|
export type ScheduledTaskPayload = {
|
|
8
8
|
preRunCommand: string | null
|
|
@@ -194,7 +194,7 @@ export function formatSessionSleepWakePrompt({
|
|
|
194
194
|
}): string {
|
|
195
195
|
const until = formatSessionSleepWakeAt(wakeAt)
|
|
196
196
|
const reasonLine = reason?.trim() ? `\nReason: ${reason.trim()}` : ''
|
|
197
|
-
return `${
|
|
197
|
+
return `${asSubtext(`Woke after sleeping until ${until}`)}${reasonLine}\nContinue the work you were waiting for.`
|
|
198
198
|
}
|
|
199
199
|
|
|
200
200
|
// Tool result after persist. Keep "Sleeping until" first so e2e matchers still hit.
|
|
@@ -714,7 +714,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
714
714
|
> running create file
|
|
715
715
|
> creating marker
|
|
716
716
|
|
|
717
|
-
┣ bash _Create marker file for e2e test_
|
|
717
|
+
-# ┣ bash _Create marker file for e2e test_
|
|
718
718
|
|
|
719
719
|
> file created
|
|
720
720
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
@@ -202,7 +202,7 @@ e2eTest('/undo sets revert state and cleans up on next prompt', () => {
|
|
|
202
202
|
-# *using deterministic-provider/deterministic-v2*
|
|
203
203
|
> creating undo file
|
|
204
204
|
|
|
205
|
-
┣ bash _Create undo marker file_
|
|
205
|
+
-# ┣ bash _Create undo marker file_
|
|
206
206
|
|
|
207
207
|
> undo file created
|
|
208
208
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|