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
|
@@ -1542,12 +1542,107 @@ describe('getPromptCacheClear', () => {
|
|
|
1542
1542
|
sessionId,
|
|
1543
1543
|
currentMessageId: 'msg_asst_2',
|
|
1544
1544
|
})).toEqual({
|
|
1545
|
-
|
|
1545
|
+
expectedCacheRead: 20000,
|
|
1546
1546
|
currentCacheRead: 0,
|
|
1547
1547
|
previousMessageId: 'msg_asst_1',
|
|
1548
1548
|
currentMessageId: 'msg_asst_2',
|
|
1549
1549
|
});
|
|
1550
1550
|
});
|
|
1551
|
+
test('detects a miss on the second turn when the first turn only wrote cache', () => {
|
|
1552
|
+
const sessionId = 'ses_cache_second_turn';
|
|
1553
|
+
const events = [
|
|
1554
|
+
userEvent({ sessionId, messageId: 'msg_user_1', created: 1 }),
|
|
1555
|
+
assistantEvent({
|
|
1556
|
+
sessionId,
|
|
1557
|
+
messageId: 'msg_asst_1',
|
|
1558
|
+
parentID: 'msg_user_1',
|
|
1559
|
+
created: 2,
|
|
1560
|
+
tokens: {
|
|
1561
|
+
input: 100,
|
|
1562
|
+
output: 10,
|
|
1563
|
+
reasoning: 0,
|
|
1564
|
+
cache: { read: 0, write: 20000 },
|
|
1565
|
+
},
|
|
1566
|
+
}),
|
|
1567
|
+
userEvent({ sessionId, messageId: 'msg_user_2', created: 3 }),
|
|
1568
|
+
assistantEvent({
|
|
1569
|
+
sessionId,
|
|
1570
|
+
messageId: 'msg_asst_2',
|
|
1571
|
+
parentID: 'msg_user_2',
|
|
1572
|
+
created: 4,
|
|
1573
|
+
tokens: {
|
|
1574
|
+
input: 100,
|
|
1575
|
+
output: 12,
|
|
1576
|
+
reasoning: 0,
|
|
1577
|
+
cache: { read: 0, write: 20200 },
|
|
1578
|
+
},
|
|
1579
|
+
}),
|
|
1580
|
+
];
|
|
1581
|
+
expect(getPromptCacheClear({
|
|
1582
|
+
events,
|
|
1583
|
+
sessionId,
|
|
1584
|
+
currentMessageId: 'msg_asst_2',
|
|
1585
|
+
})).toEqual({
|
|
1586
|
+
expectedCacheRead: 20000,
|
|
1587
|
+
currentCacheRead: 0,
|
|
1588
|
+
previousMessageId: 'msg_asst_1',
|
|
1589
|
+
currentMessageId: 'msg_asst_2',
|
|
1590
|
+
});
|
|
1591
|
+
});
|
|
1592
|
+
test('detects consecutive misses, not only every other turn', () => {
|
|
1593
|
+
const sessionId = 'ses_cache_consecutive';
|
|
1594
|
+
const events = [
|
|
1595
|
+
userEvent({ sessionId, messageId: 'msg_user_1', created: 1 }),
|
|
1596
|
+
assistantEvent({
|
|
1597
|
+
sessionId,
|
|
1598
|
+
messageId: 'msg_asst_1',
|
|
1599
|
+
parentID: 'msg_user_1',
|
|
1600
|
+
created: 2,
|
|
1601
|
+
tokens: {
|
|
1602
|
+
input: 100,
|
|
1603
|
+
output: 10,
|
|
1604
|
+
reasoning: 0,
|
|
1605
|
+
cache: { read: 20000, write: 100 },
|
|
1606
|
+
},
|
|
1607
|
+
}),
|
|
1608
|
+
userEvent({ sessionId, messageId: 'msg_user_2', created: 3 }),
|
|
1609
|
+
assistantEvent({
|
|
1610
|
+
sessionId,
|
|
1611
|
+
messageId: 'msg_asst_2',
|
|
1612
|
+
parentID: 'msg_user_2',
|
|
1613
|
+
created: 4,
|
|
1614
|
+
tokens: {
|
|
1615
|
+
input: 100,
|
|
1616
|
+
output: 10,
|
|
1617
|
+
reasoning: 0,
|
|
1618
|
+
cache: { read: 0, write: 20300 },
|
|
1619
|
+
},
|
|
1620
|
+
}),
|
|
1621
|
+
userEvent({ sessionId, messageId: 'msg_user_3', created: 5 }),
|
|
1622
|
+
assistantEvent({
|
|
1623
|
+
sessionId,
|
|
1624
|
+
messageId: 'msg_asst_3',
|
|
1625
|
+
parentID: 'msg_user_3',
|
|
1626
|
+
created: 6,
|
|
1627
|
+
tokens: {
|
|
1628
|
+
input: 100,
|
|
1629
|
+
output: 10,
|
|
1630
|
+
reasoning: 0,
|
|
1631
|
+
cache: { read: 0, write: 20500 },
|
|
1632
|
+
},
|
|
1633
|
+
}),
|
|
1634
|
+
];
|
|
1635
|
+
expect(getPromptCacheClear({
|
|
1636
|
+
events,
|
|
1637
|
+
sessionId,
|
|
1638
|
+
currentMessageId: 'msg_asst_3',
|
|
1639
|
+
})).toEqual({
|
|
1640
|
+
expectedCacheRead: 20300,
|
|
1641
|
+
currentCacheRead: 0,
|
|
1642
|
+
previousMessageId: 'msg_asst_2',
|
|
1643
|
+
currentMessageId: 'msg_asst_3',
|
|
1644
|
+
});
|
|
1645
|
+
});
|
|
1551
1646
|
test('detects a cache drop when prompt size stays similar', () => {
|
|
1552
1647
|
const sessionId = 'ses_cache_drop';
|
|
1553
1648
|
const events = [
|
|
@@ -1583,7 +1678,7 @@ describe('getPromptCacheClear', () => {
|
|
|
1583
1678
|
sessionId,
|
|
1584
1679
|
currentMessageId: 'msg_asst_2',
|
|
1585
1680
|
})).toEqual({
|
|
1586
|
-
|
|
1681
|
+
expectedCacheRead: 40000,
|
|
1587
1682
|
currentCacheRead: 8000,
|
|
1588
1683
|
previousMessageId: 'msg_asst_1',
|
|
1589
1684
|
currentMessageId: 'msg_asst_2',
|
|
@@ -1679,8 +1774,8 @@ describe('getPromptCacheClear', () => {
|
|
|
1679
1774
|
currentMessageId: 'msg_asst_2',
|
|
1680
1775
|
})).toBeUndefined();
|
|
1681
1776
|
});
|
|
1682
|
-
test('ignores a smaller prompt after
|
|
1683
|
-
const sessionId = '
|
|
1777
|
+
test('ignores a smaller prompt that is still read from cache after a revert', () => {
|
|
1778
|
+
const sessionId = 'ses_cache_revert';
|
|
1684
1779
|
const events = [
|
|
1685
1780
|
userEvent({ sessionId, messageId: 'msg_user_1', created: 1 }),
|
|
1686
1781
|
assistantEvent({
|
|
@@ -1702,10 +1797,10 @@ describe('getPromptCacheClear', () => {
|
|
|
1702
1797
|
parentID: 'msg_user_2',
|
|
1703
1798
|
created: 4,
|
|
1704
1799
|
tokens: {
|
|
1705
|
-
input:
|
|
1800
|
+
input: 200,
|
|
1706
1801
|
output: 10,
|
|
1707
1802
|
reasoning: 0,
|
|
1708
|
-
cache: { read: 8000, write:
|
|
1803
|
+
cache: { read: 8000, write: 0 },
|
|
1709
1804
|
},
|
|
1710
1805
|
}),
|
|
1711
1806
|
];
|
|
@@ -1715,6 +1810,47 @@ describe('getPromptCacheClear', () => {
|
|
|
1715
1810
|
currentMessageId: 'msg_asst_2',
|
|
1716
1811
|
})).toBeUndefined();
|
|
1717
1812
|
});
|
|
1813
|
+
test('detects a miss when the prompt shrank without pruning', () => {
|
|
1814
|
+
const sessionId = 'ses_cache_shrunk_miss';
|
|
1815
|
+
const events = [
|
|
1816
|
+
userEvent({ sessionId, messageId: 'msg_user_1', created: 1 }),
|
|
1817
|
+
assistantEvent({
|
|
1818
|
+
sessionId,
|
|
1819
|
+
messageId: 'msg_asst_1',
|
|
1820
|
+
parentID: 'msg_user_1',
|
|
1821
|
+
created: 2,
|
|
1822
|
+
tokens: {
|
|
1823
|
+
input: 100,
|
|
1824
|
+
output: 10,
|
|
1825
|
+
reasoning: 0,
|
|
1826
|
+
cache: { read: 187000, write: 0 },
|
|
1827
|
+
},
|
|
1828
|
+
}),
|
|
1829
|
+
userEvent({ sessionId, messageId: 'msg_user_2', created: 3 }),
|
|
1830
|
+
assistantEvent({
|
|
1831
|
+
sessionId,
|
|
1832
|
+
messageId: 'msg_asst_2',
|
|
1833
|
+
parentID: 'msg_user_2',
|
|
1834
|
+
created: 4,
|
|
1835
|
+
tokens: {
|
|
1836
|
+
input: 182000,
|
|
1837
|
+
output: 10,
|
|
1838
|
+
reasoning: 0,
|
|
1839
|
+
cache: { read: 0, write: 0 },
|
|
1840
|
+
},
|
|
1841
|
+
}),
|
|
1842
|
+
];
|
|
1843
|
+
expect(getPromptCacheClear({
|
|
1844
|
+
events,
|
|
1845
|
+
sessionId,
|
|
1846
|
+
currentMessageId: 'msg_asst_2',
|
|
1847
|
+
})).toEqual({
|
|
1848
|
+
expectedCacheRead: 182000,
|
|
1849
|
+
currentCacheRead: 0,
|
|
1850
|
+
previousMessageId: 'msg_asst_1',
|
|
1851
|
+
currentMessageId: 'msg_asst_2',
|
|
1852
|
+
});
|
|
1853
|
+
});
|
|
1718
1854
|
test('compares against the previous user turn, not a same-turn tool step', () => {
|
|
1719
1855
|
const sessionId = 'ses_cache_same_turn';
|
|
1720
1856
|
const events = [
|
|
@@ -1762,7 +1898,7 @@ describe('getPromptCacheClear', () => {
|
|
|
1762
1898
|
sessionId,
|
|
1763
1899
|
currentMessageId: 'msg_asst_2b',
|
|
1764
1900
|
})).toEqual({
|
|
1765
|
-
|
|
1901
|
+
expectedCacheRead: 20000,
|
|
1766
1902
|
currentCacheRead: 0,
|
|
1767
1903
|
previousMessageId: 'msg_asst_1',
|
|
1768
1904
|
currentMessageId: 'msg_asst_2b',
|
|
@@ -1804,7 +1940,7 @@ describe('getPromptCacheClear', () => {
|
|
|
1804
1940
|
currentMessageId: 'msg_asst_2',
|
|
1805
1941
|
})).toBeUndefined();
|
|
1806
1942
|
});
|
|
1807
|
-
test('
|
|
1943
|
+
test('compares across an aborted assistant with the last good reply', () => {
|
|
1808
1944
|
const sessionId = 'ses_cache_abort_bridge';
|
|
1809
1945
|
const events = [
|
|
1810
1946
|
userEvent({ sessionId, messageId: 'msg_user_1', created: 1 }),
|
|
@@ -1852,7 +1988,12 @@ describe('getPromptCacheClear', () => {
|
|
|
1852
1988
|
events,
|
|
1853
1989
|
sessionId,
|
|
1854
1990
|
currentMessageId: 'msg_asst_3',
|
|
1855
|
-
})).
|
|
1991
|
+
})).toEqual({
|
|
1992
|
+
expectedCacheRead: 40000,
|
|
1993
|
+
currentCacheRead: 0,
|
|
1994
|
+
previousMessageId: 'msg_asst_1',
|
|
1995
|
+
currentMessageId: 'msg_asst_3',
|
|
1996
|
+
});
|
|
1856
1997
|
});
|
|
1857
1998
|
test('does not compare across a compaction summary', () => {
|
|
1858
1999
|
const sessionId = 'ses_cache_compaction';
|
|
@@ -1993,13 +2134,13 @@ describe('getPromptCacheClear', () => {
|
|
|
1993
2134
|
});
|
|
1994
2135
|
test('formats a compact cache-miss notice', () => {
|
|
1995
2136
|
expect(formatPromptCacheClearMessage({
|
|
1996
|
-
|
|
2137
|
+
expectedCacheRead: 20000,
|
|
1997
2138
|
currentCacheRead: 0,
|
|
1998
2139
|
previousMessageId: 'msg_asst_1',
|
|
1999
2140
|
currentMessageId: 'msg_asst_2',
|
|
2000
2141
|
})).toBe('prompt cache missed (20k → 0)');
|
|
2001
2142
|
expect(formatPromptCacheClearMessage({
|
|
2002
|
-
|
|
2143
|
+
expectedCacheRead: 20000,
|
|
2003
2144
|
currentCacheRead: 0,
|
|
2004
2145
|
previousMessageId: 'msg_asst_1',
|
|
2005
2146
|
currentMessageId: 'msg_asst_2',
|
|
@@ -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, QUEUE_PREFIX, 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';
|
|
@@ -1653,14 +1653,15 @@ export class ThreadSessionRuntime {
|
|
|
1653
1653
|
upToIndex: this.eventBuffer.length - 2,
|
|
1654
1654
|
});
|
|
1655
1655
|
const completedAt = msg.time.completed;
|
|
1656
|
-
if (!wasAlreadyCompleted
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1656
|
+
if (!wasAlreadyCompleted && typeof completedAt === 'number') {
|
|
1657
|
+
if (isAssistantMessageNaturalCompletion({ message: msg })) {
|
|
1658
|
+
await this.handleNaturalAssistantCompletion({
|
|
1659
|
+
completedMessageId: msg.id,
|
|
1660
|
+
completedAt,
|
|
1661
|
+
});
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
await this.maybeNotifyPromptCacheClear({ sessionId, messageId: msg.id });
|
|
1664
1665
|
}
|
|
1665
1666
|
// Context usage notice.
|
|
1666
1667
|
// Skip the final assistant update for a run: by the time the last
|
|
@@ -1693,7 +1694,7 @@ export class ThreadSessionRuntime {
|
|
|
1693
1694
|
return;
|
|
1694
1695
|
}
|
|
1695
1696
|
this.lastDisplayedContextPercentage = thresholdCrossed;
|
|
1696
|
-
const chunk =
|
|
1697
|
+
const chunk = asSubtext(`context usage ${currentPercentage}%`);
|
|
1697
1698
|
const sendResult = await this.thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
1698
1699
|
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }));
|
|
1699
1700
|
if (sendResult instanceof Error) {
|
|
@@ -1877,7 +1878,7 @@ export class ThreadSessionRuntime {
|
|
|
1877
1878
|
}
|
|
1878
1879
|
return ` (${pct.toFixed(1)}%)`;
|
|
1879
1880
|
})();
|
|
1880
|
-
const chunk = `${STATUS_PREFIX}${part.tool} returned ${formattedTokens} tokens${percentageSuffix}
|
|
1881
|
+
const chunk = asSubtext(`${STATUS_PREFIX}${part.tool} returned ${formattedTokens} tokens${percentageSuffix}`);
|
|
1881
1882
|
const largeOutputResult = await this.thread.send({
|
|
1882
1883
|
content: chunk,
|
|
1883
1884
|
flags: SILENT_MESSAGE_FLAGS,
|
|
@@ -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) {
|
|
@@ -3022,17 +3023,20 @@ export class ThreadSessionRuntime {
|
|
|
3022
3023
|
apiAbortPromise,
|
|
3023
3024
|
};
|
|
3024
3025
|
}
|
|
3025
|
-
|
|
3026
|
+
/** Explicit user abort: stops the run and drops all queued messages. Returns the removed items. */
|
|
3027
|
+
async abortActiveRun(reason) {
|
|
3026
3028
|
const outcome = this.abortActiveRunInternal({
|
|
3027
3029
|
reason,
|
|
3028
3030
|
});
|
|
3029
3031
|
if (outcome.apiAbortPromise) {
|
|
3030
3032
|
void outcome.apiAbortPromise;
|
|
3031
3033
|
}
|
|
3032
|
-
//
|
|
3033
|
-
|
|
3034
|
-
|
|
3034
|
+
// Enqueued synchronously, so it runs before the abort's session.idle can drain the queue.
|
|
3035
|
+
let cleared = [];
|
|
3036
|
+
await this.dispatchAction(async () => {
|
|
3037
|
+
cleared = await this.clearQueueNow();
|
|
3035
3038
|
});
|
|
3039
|
+
return cleared;
|
|
3036
3040
|
}
|
|
3037
3041
|
async abortActiveRunAndWait({ reason, timeoutMs = 2_000, }) {
|
|
3038
3042
|
const state = this.state;
|
|
@@ -3073,17 +3077,21 @@ export class ThreadSessionRuntime {
|
|
|
3073
3077
|
async clearQueue() {
|
|
3074
3078
|
let cleared = [];
|
|
3075
3079
|
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);
|
|
3080
|
+
cleared = await this.clearQueueNow();
|
|
3084
3081
|
});
|
|
3085
3082
|
return cleared;
|
|
3086
3083
|
}
|
|
3084
|
+
// Must run inside dispatchAction.
|
|
3085
|
+
async clearQueueNow() {
|
|
3086
|
+
const persistResult = await deleteThreadQueueItems(this.threadId).catch((error) => {
|
|
3087
|
+
return new Error('Failed to clear persisted queue', { cause: error });
|
|
3088
|
+
});
|
|
3089
|
+
if (persistResult instanceof Error) {
|
|
3090
|
+
logger.error(`[QUEUE] Failed to clear persisted queue for thread ${this.threadId}: ${persistResult.message}`);
|
|
3091
|
+
return [];
|
|
3092
|
+
}
|
|
3093
|
+
return threadState.clearQueueItems(this.threadId);
|
|
3094
|
+
}
|
|
3087
3095
|
/** Remove a queued message by its 1-based position. */
|
|
3088
3096
|
async removeQueuePosition(position) {
|
|
3089
3097
|
let removed;
|
|
@@ -3912,7 +3920,7 @@ export class ThreadSessionRuntime {
|
|
|
3912
3920
|
const agentLabel = agent && agent.toLowerCase() !== 'build'
|
|
3913
3921
|
? ` ⋅ ${agent}`
|
|
3914
3922
|
: '';
|
|
3915
|
-
const result = await sendThreadMessage(this.thread,
|
|
3923
|
+
const result = await sendThreadMessage(this.thread, asSubtext(`*using ${modelLabel}${agentLabel}*`), { flags: SILENT_MESSAGE_FLAGS }).catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }));
|
|
3916
3924
|
if (result instanceof Error) {
|
|
3917
3925
|
logger.warn(`[SESSION INFO] Failed to send model info: ${result.message}`);
|
|
3918
3926
|
}
|
|
@@ -4041,7 +4049,7 @@ export class ThreadSessionRuntime {
|
|
|
4041
4049
|
})
|
|
4042
4050
|
: undefined;
|
|
4043
4051
|
const mention = mentionUserId ? ` <@${mentionUserId}>` : '';
|
|
4044
|
-
const footerText =
|
|
4052
|
+
const footerText = asSubtext(`*${projectInfo}${sessionDuration}${contextInfo}${modelInfo}${agentInfo}*${mention}`);
|
|
4045
4053
|
this.stopTyping();
|
|
4046
4054
|
await sendThreadMessage(this.thread, footerText, {
|
|
4047
4055
|
flags: shouldNotifyUser ? NOTIFY_MESSAGE_FLAGS : SILENT_MESSAGE_FLAGS,
|
|
@@ -4057,6 +4065,11 @@ export class ThreadSessionRuntime {
|
|
|
4057
4065
|
this.lastSentPartKind = undefined;
|
|
4058
4066
|
}
|
|
4059
4067
|
async maybeNotifyPromptCacheClear({ sessionId, messageId, }) {
|
|
4068
|
+
// Only the first reply after a user prompt can show a cold cache. Later steps re-read the turn's own writes.
|
|
4069
|
+
const [firstAssistantId] = this.getAssistantMessageIdsForCurrentTurn({ sessionId });
|
|
4070
|
+
if (firstAssistantId !== messageId) {
|
|
4071
|
+
return;
|
|
4072
|
+
}
|
|
4060
4073
|
const cacheClear = getPromptCacheClear({
|
|
4061
4074
|
events: this.eventBuffer,
|
|
4062
4075
|
sessionId,
|
|
@@ -4070,7 +4083,7 @@ export class ThreadSessionRuntime {
|
|
|
4070
4083
|
previousMessageId: cacheClear.previousMessageId,
|
|
4071
4084
|
currentMessageId: cacheClear.currentMessageId,
|
|
4072
4085
|
});
|
|
4073
|
-
const chunk =
|
|
4086
|
+
const chunk = asSubtext(formatPromptCacheClearMessage(cacheClear, systemDiff));
|
|
4074
4087
|
const sendResult = await this.thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
|
|
4075
4088
|
.catch((e) => new DiscordOperationError({ operation: 'sendMessage', cause: e }));
|
|
4076
4089
|
if (sendResult instanceof Error) {
|
package/dist/sleep.e2e.test.js
CHANGED
|
@@ -313,7 +313,7 @@ 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*
|
|
@@ -412,7 +412,7 @@ 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*
|
package/dist/task-runner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Scheduled task runner for due `send --send-at` jobs and session sleep wakes.
|
|
2
2
|
import { DiscordAPIError, Routes } from 'discord.js';
|
|
3
3
|
import { createDiscordRest } from './discord-urls.js';
|
|
4
|
-
import { ensureThreadMember } from './discord-utils.js';
|
|
4
|
+
import { buildThreadStartEmbeds, ensureThreadMember } from './discord-utils.js';
|
|
5
5
|
import YAML from 'yaml';
|
|
6
6
|
import { claimScheduledTaskRunning, createScheduledTaskRun, failScheduledTaskRun, finishScheduledTaskRun, getDuePlannedScheduledTasks, getScheduledTask, getActiveScheduledTaskRuns, markScheduledTaskCronRescheduled, markScheduledTaskCronRetry, markScheduledTaskFailed, deleteScheduledTask, recoverStaleRunningScheduledTasks, releaseScheduledTaskClaim, setScheduledTaskRunThread, claimSessionSleepAttempt, getDueSessionSleeps, getThreadIdBySessionId, markSessionSleepFailed, } from './database.js';
|
|
7
7
|
import { execAsync } from './exec-async.js';
|
|
@@ -44,7 +44,7 @@ async function executeThreadScheduledTask({ rest, task, payload, prompt, runId,
|
|
|
44
44
|
: {}),
|
|
45
45
|
...(payload.parentSessionId ? { parentSessionId: payload.parentSessionId } : {}),
|
|
46
46
|
};
|
|
47
|
-
const embed =
|
|
47
|
+
const embed = await buildThreadStartEmbeds(marker);
|
|
48
48
|
// Newline between prefix and prompt so leading /command detection can
|
|
49
49
|
// find the command on its own line.
|
|
50
50
|
const prefixedPrompt = `${QUEUE_PREFIX}**kimaki-cli:**\n${prompt}`;
|
|
@@ -188,7 +188,7 @@ async function executeChannelScheduledTask({ rest, task, payload, prompt, runId,
|
|
|
188
188
|
...(payload.parentSessionId ? { parentSessionId: payload.parentSessionId } : {}),
|
|
189
189
|
};
|
|
190
190
|
const embeds = marker
|
|
191
|
-
?
|
|
191
|
+
? await buildThreadStartEmbeds(marker)
|
|
192
192
|
: undefined;
|
|
193
193
|
const starterResult = await rest
|
|
194
194
|
.post(Routes.channelMessages(payload.channelId), {
|
package/dist/task-schedule.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Scheduled task parsing for `send --send-at`, plus kimaki_sleep wake/result text.
|
|
2
2
|
import { CronExpressionParser } from 'cron-parser';
|
|
3
3
|
import * as errore from 'errore';
|
|
4
|
-
import {
|
|
4
|
+
import { asSubtext } from './message-formatting.js';
|
|
5
5
|
const UTC_SEND_AT_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?Z$/;
|
|
6
6
|
export function getLocalTimeZone() {
|
|
7
7
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -88,7 +88,7 @@ export function formatSessionSleepWakeAt(wakeAt) {
|
|
|
88
88
|
export function formatSessionSleepWakePrompt({ wakeAt, reason, }) {
|
|
89
89
|
const until = formatSessionSleepWakeAt(wakeAt);
|
|
90
90
|
const reasonLine = reason?.trim() ? `\nReason: ${reason.trim()}` : '';
|
|
91
|
-
return `${
|
|
91
|
+
return `${asSubtext(`Woke after sleeping until ${until}`)}${reasonLine}\nContinue the work you were waiting for.`;
|
|
92
92
|
}
|
|
93
93
|
// Tool result after persist. Keep "Sleeping until" first so e2e matchers still hit.
|
|
94
94
|
// Do not say "woken" or "Woke after": models treat that as the wake turn.
|
|
@@ -583,7 +583,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
583
583
|
> running create file
|
|
584
584
|
> creating marker
|
|
585
585
|
|
|
586
|
-
┣ bash _Create marker file for e2e test_
|
|
586
|
+
-# ┣ bash _Create marker file for e2e test_
|
|
587
587
|
|
|
588
588
|
> file created
|
|
589
589
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
@@ -156,7 +156,7 @@ e2eTest('/undo sets revert state and cleans up on next prompt', () => {
|
|
|
156
156
|
-# *using deterministic-provider/deterministic-v2*
|
|
157
157
|
> creating undo file
|
|
158
158
|
|
|
159
|
-
┣ bash _Create undo marker file_
|
|
159
|
+
-# ┣ bash _Create undo marker file_
|
|
160
160
|
|
|
161
161
|
> undo file created
|
|
162
162
|
-# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.30.
|
|
5
|
+
"version": "0.30.1",
|
|
6
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
7
7
|
"bin": "bin.js",
|
|
8
8
|
"exports": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"undici": "^8.0.2",
|
|
34
34
|
"discord-digital-twin": "^0.1.2",
|
|
35
35
|
"opencode-cached-provider": "^0.0.1",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"opencode-deterministic-provider": "^0.0.1",
|
|
37
|
+
"db": "^0.0.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@ai-sdk/google": "^3.0.53",
|
|
@@ -71,11 +71,11 @@
|
|
|
71
71
|
"zod": "^4.3.6",
|
|
72
72
|
"zustand": "^5.0.11",
|
|
73
73
|
"@subrouter/cli": "^0.6.1",
|
|
74
|
-
"@subrouter/opencode": "^0.5.1",
|
|
75
74
|
"errore": "^0.14.1",
|
|
76
|
-
"libsqlproxy": "^0.1.0",
|
|
77
75
|
"opencode-injection-guard": "^0.2.1",
|
|
78
|
-
"
|
|
76
|
+
"libsqlproxy": "^0.1.0",
|
|
77
|
+
"traforo": "^0.7.2",
|
|
78
|
+
"@subrouter/opencode": "^0.5.1"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
81
|
"@snazzah/davey": "^0.1.10",
|
package/skills/holocron/SKILL.md
CHANGED
|
@@ -746,34 +746,6 @@ npx -y "@holocron.so/cli" whoami
|
|
|
746
746
|
If `whoami` succeeds and shows user, orgs, and projects, skip login. Only run
|
|
747
747
|
`npx -y "@holocron.so/cli" login` if `whoami` fails.
|
|
748
748
|
|
|
749
|
-
## www hostname
|
|
750
|
-
|
|
751
|
-
For every new site on a custom domain, **301 `www` to the apex in one hop**.
|
|
752
|
-
Never leave `www` on another host (Vercel, Pages, a second worker). Never use
|
|
753
|
-
307. Chain `http://www` → `https://www` → apex is two hops; collapse it.
|
|
754
|
-
|
|
755
|
-
On Cloudflare Workers:
|
|
756
|
-
|
|
757
|
-
1. Add `www.example.com` as a `custom_domain` on the **same** worker as the apex.
|
|
758
|
-
2. Redirect `www` to `https://example.com` + path + query with status **301**.
|
|
759
|
-
|
|
760
|
-
```ts
|
|
761
|
-
.use(({ request }, next) => {
|
|
762
|
-
const url = new URL(request.url)
|
|
763
|
-
if (!url.hostname.startsWith('www.')) return next()
|
|
764
|
-
url.hostname = url.hostname.slice('www.'.length)
|
|
765
|
-
url.protocol = 'https:'
|
|
766
|
-
throw redirect(url.toString(), { status: 301 })
|
|
767
|
-
})
|
|
768
|
-
```
|
|
769
|
-
|
|
770
|
-
```jsonc
|
|
771
|
-
"routes": [
|
|
772
|
-
{ "pattern": "example.com", "custom_domain": true },
|
|
773
|
-
{ "pattern": "www.example.com", "custom_domain": true }
|
|
774
|
-
]
|
|
775
|
-
```
|
|
776
|
-
|
|
777
749
|
## Deploy
|
|
778
750
|
|
|
779
751
|
When the user asks to "deploy the site", they may mean deploying with their own
|
package/src/cli-commands/send.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { goke } from 'goke'
|
|
7
7
|
import { z } from 'zod'
|
|
8
8
|
import { note } from '@clack/prompts'
|
|
9
|
-
import YAML from 'yaml'
|
|
10
9
|
import * as errore from 'errore'
|
|
11
10
|
import type { OpencodeClient, Event as OpenCodeEvent } from '@opencode-ai/sdk/v2'
|
|
12
11
|
import { Events, ActivityType, type PresenceStatusData, type Guild, Routes } from 'discord.js'
|
|
@@ -30,7 +29,7 @@ import { QUEUE_PREFIX } from '../message-formatting.js'
|
|
|
30
29
|
import type { ThreadStartMarker } from '../system-message.js'
|
|
31
30
|
import { buildOpencodeEventLogLine } from '../session-handler/opencode-session-event-log.js'
|
|
32
31
|
import { createDiscordRest } from '../discord-urls.js'
|
|
33
|
-
import { archiveThread, ensureThreadMember, uploadFilesToDiscord, stripMentions } from '../discord-utils.js'
|
|
32
|
+
import { archiveThread, buildThreadStartEmbeds, ensureThreadMember, uploadFilesToDiscord, stripMentions } from '../discord-utils.js'
|
|
34
33
|
import { setDataDir, setProjectsDir, getDataDir, getProjectsDir } from '../config.js'
|
|
35
34
|
import { execAsync, resolveSessionWorkingDirectory, isGitRepositoryRoot } from '../worktrees.js'
|
|
36
35
|
import { upgrade, getCurrentVersion } from '../upgrade.js'
|
|
@@ -565,12 +564,7 @@ cli
|
|
|
565
564
|
...(options.injectionGuard?.length ? { injectionGuardPatterns: options.injectionGuard } : {}),
|
|
566
565
|
...(options.parentSession && { parentSessionId: options.parentSession }),
|
|
567
566
|
}
|
|
568
|
-
const promptEmbed =
|
|
569
|
-
{
|
|
570
|
-
color: 0x2b2d31,
|
|
571
|
-
footer: { text: YAML.stringify(threadPromptMarker) },
|
|
572
|
-
},
|
|
573
|
-
]
|
|
567
|
+
const promptEmbed = await buildThreadStartEmbeds(threadPromptMarker)
|
|
574
568
|
|
|
575
569
|
// Prefix the prompt so it's clear who sent it (matches /queue format).
|
|
576
570
|
// Use a newline between prefix and prompt so leading /command
|
|
@@ -779,7 +773,7 @@ cli
|
|
|
779
773
|
...(options.parentSession && { parentSessionId: options.parentSession }),
|
|
780
774
|
}
|
|
781
775
|
const autoStartEmbed = embedMarker
|
|
782
|
-
?
|
|
776
|
+
? await buildThreadStartEmbeds(embedMarker)
|
|
783
777
|
: undefined
|
|
784
778
|
|
|
785
779
|
const starterMessage = await sendDiscordMessageWithOptionalAttachment({
|
package/src/commands/abort.ts
CHANGED
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
type ThreadChannel,
|
|
8
8
|
} from 'discord.js'
|
|
9
9
|
import type { CommandContext } from './types.js'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
cancelSessionSleepForThread,
|
|
12
|
+
deleteThreadQueueItems,
|
|
13
|
+
getThreadSession,
|
|
14
|
+
} from '../database.js'
|
|
11
15
|
import { getOpencodeClient, initializeOpencodeForDirectory } from '../opencode.js'
|
|
12
16
|
import {
|
|
13
17
|
resolveWorkingDirectory,
|
|
@@ -71,11 +75,20 @@ export async function handleAbortCommand({
|
|
|
71
75
|
// wake would still fire later and restart a session the user just stopped.
|
|
72
76
|
await cancelSessionSleepForThread({ threadId: channel.id })
|
|
73
77
|
|
|
74
|
-
// abortActiveRun delegates to session.abort()
|
|
78
|
+
// abortActiveRun delegates to session.abort() and clears the /queue, so
|
|
79
|
+
// queued messages are not sent after abort or restored after a restart.
|
|
75
80
|
const runtime = getRuntime(channel.id)
|
|
81
|
+
let clearedCount = 0
|
|
76
82
|
if (runtime) {
|
|
77
|
-
runtime.abortActiveRun('user-requested')
|
|
83
|
+
const cleared = await runtime.abortActiveRun('user-requested')
|
|
84
|
+
clearedCount = cleared.length
|
|
78
85
|
} else {
|
|
86
|
+
const deleteResult = await deleteThreadQueueItems(channel.id).catch((error) => {
|
|
87
|
+
return new Error('Failed to clear persisted queue', { cause: error })
|
|
88
|
+
})
|
|
89
|
+
if (deleteResult instanceof Error) {
|
|
90
|
+
logger.error(`[ABORT] ${deleteResult.message}:`, deleteResult.cause)
|
|
91
|
+
}
|
|
79
92
|
// No runtime but session exists — fall back to direct API abort
|
|
80
93
|
const serverResult = await initializeOpencodeForDirectory(projectDirectory)
|
|
81
94
|
if (serverResult instanceof Error) {
|
|
@@ -92,6 +105,9 @@ export async function handleAbortCommand({
|
|
|
92
105
|
}
|
|
93
106
|
}
|
|
94
107
|
|
|
95
|
-
|
|
108
|
+
const queueNote = clearedCount > 0
|
|
109
|
+
? `, cleared ${clearedCount} queued message${clearedCount > 1 ? 's' : ''}`
|
|
110
|
+
: ''
|
|
111
|
+
await command.editReply(`Request **aborted**${queueNote}`)
|
|
96
112
|
logger.log(`Session ${sessionId} aborted by user`)
|
|
97
113
|
}
|