kimaki 0.26.0 → 0.28.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 +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Tests for the durable session-sleep delivery protocol.
|
|
2
|
+
//
|
|
3
|
+
// The invariant under test: a wake is delivered at least once and turned into a
|
|
4
|
+
// session turn at most once. A row therefore stays `planned` until ingress
|
|
5
|
+
// consumes it, and every delivery write is guarded on delivery_id so a stale
|
|
6
|
+
// in-flight wake can never touch a newer sleep occurrence.
|
|
7
|
+
|
|
8
|
+
import { afterAll, describe, expect, test } from 'vitest'
|
|
9
|
+
import {
|
|
10
|
+
cancelSessionSleepForThread,
|
|
11
|
+
claimSessionSleepAttempt,
|
|
12
|
+
consumeSessionSleepWake,
|
|
13
|
+
getDueSessionSleeps,
|
|
14
|
+
getSessionSleep,
|
|
15
|
+
getThreadIdBySessionId,
|
|
16
|
+
markSessionSleepFailed,
|
|
17
|
+
setThreadSession,
|
|
18
|
+
upsertSessionSleep,
|
|
19
|
+
} from './database.js'
|
|
20
|
+
import { closeDb } from './db.js'
|
|
21
|
+
import { buildSessionSleepWakeBody } from './task-runner.js'
|
|
22
|
+
|
|
23
|
+
const RETRY_AFTER_MS = 30_000
|
|
24
|
+
const NOW = new Date('2026-08-19T12:00:00Z')
|
|
25
|
+
const DUE = new Date('2026-08-19T11:00:00Z')
|
|
26
|
+
|
|
27
|
+
async function createSleep({
|
|
28
|
+
sessionId,
|
|
29
|
+
threadId,
|
|
30
|
+
wakeAt = DUE,
|
|
31
|
+
reason = null,
|
|
32
|
+
}: {
|
|
33
|
+
sessionId: string
|
|
34
|
+
threadId: string
|
|
35
|
+
wakeAt?: Date
|
|
36
|
+
reason?: string | null
|
|
37
|
+
}) {
|
|
38
|
+
await setThreadSession(threadId, sessionId)
|
|
39
|
+
return upsertSessionSleep({ sessionId, wakeAt, reason })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('session sleep delivery', () => {
|
|
43
|
+
test('uses a Discord-compatible nonce without changing the delivery marker', () => {
|
|
44
|
+
const deliveryId = 'f0e3a009-dd6b-4c6a-a244-646693d9ae8d'
|
|
45
|
+
const body = buildSessionSleepWakeBody({
|
|
46
|
+
deliveryId,
|
|
47
|
+
wakeAt: DUE,
|
|
48
|
+
reason: 'Continue after deployment',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
expect({
|
|
52
|
+
nonce: body.nonce,
|
|
53
|
+
nonceLength: body.nonce.length,
|
|
54
|
+
markerIncludesDeliveryId: body.embeds[0]?.footer.text.includes(deliveryId),
|
|
55
|
+
}).toMatchInlineSnapshot(`
|
|
56
|
+
{
|
|
57
|
+
"markerIncludesDeliveryId": true,
|
|
58
|
+
"nonce": "f0e3a009dd6b4c6aa24464669",
|
|
59
|
+
"nonceLength": 25,
|
|
60
|
+
}
|
|
61
|
+
`)
|
|
62
|
+
expect(body.nonce.length).toBeLessThanOrEqual(25)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('claim reserves an attempt but stays planned so a crash retries', async () => {
|
|
66
|
+
const deliveryId = await createSleep({
|
|
67
|
+
sessionId: 'ses-crash',
|
|
68
|
+
threadId: 'thr-crash',
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const claimed = await claimSessionSleepAttempt({
|
|
72
|
+
sessionId: 'ses-crash',
|
|
73
|
+
deliveryId,
|
|
74
|
+
now: NOW,
|
|
75
|
+
retryAfterMs: RETRY_AFTER_MS,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(claimed?.status).toBe('planned')
|
|
79
|
+
expect(claimed?.attempts).toBe(1)
|
|
80
|
+
|
|
81
|
+
// Simulates the process dying before the Discord post: once the retry
|
|
82
|
+
// window passes the row is due again rather than lost.
|
|
83
|
+
const laterDue = await getDueSessionSleeps({
|
|
84
|
+
now: new Date(NOW.getTime() + RETRY_AFTER_MS + 1),
|
|
85
|
+
retryAfterMs: RETRY_AFTER_MS,
|
|
86
|
+
limit: 10,
|
|
87
|
+
})
|
|
88
|
+
expect(laterDue.map((row) => row.session_id)).toContain('ses-crash')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('a second claim inside the retry window is refused', async () => {
|
|
92
|
+
const deliveryId = await createSleep({
|
|
93
|
+
sessionId: 'ses-window',
|
|
94
|
+
threadId: 'thr-window',
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const first = await claimSessionSleepAttempt({
|
|
98
|
+
sessionId: 'ses-window',
|
|
99
|
+
deliveryId,
|
|
100
|
+
now: NOW,
|
|
101
|
+
retryAfterMs: RETRY_AFTER_MS,
|
|
102
|
+
})
|
|
103
|
+
const second = await claimSessionSleepAttempt({
|
|
104
|
+
sessionId: 'ses-window',
|
|
105
|
+
deliveryId,
|
|
106
|
+
now: new Date(NOW.getTime() + 1_000),
|
|
107
|
+
retryAfterMs: RETRY_AFTER_MS,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expect(first).not.toBe(null)
|
|
111
|
+
expect(second).toBe(null)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('a new sleep occurrence invalidates an in-flight delivery', async () => {
|
|
115
|
+
const staleDeliveryId = await createSleep({
|
|
116
|
+
sessionId: 'ses-generation',
|
|
117
|
+
threadId: 'thr-generation',
|
|
118
|
+
})
|
|
119
|
+
const freshDeliveryId = await createSleep({
|
|
120
|
+
sessionId: 'ses-generation',
|
|
121
|
+
threadId: 'thr-generation',
|
|
122
|
+
wakeAt: new Date('2026-08-19T11:30:00Z'),
|
|
123
|
+
})
|
|
124
|
+
expect(freshDeliveryId).not.toBe(staleDeliveryId)
|
|
125
|
+
|
|
126
|
+
const staleClaim = await claimSessionSleepAttempt({
|
|
127
|
+
sessionId: 'ses-generation',
|
|
128
|
+
deliveryId: staleDeliveryId,
|
|
129
|
+
now: NOW,
|
|
130
|
+
retryAfterMs: RETRY_AFTER_MS,
|
|
131
|
+
})
|
|
132
|
+
// The wake message already in flight for the old occurrence must not be
|
|
133
|
+
// able to deliver against the new one.
|
|
134
|
+
const staleConsume = await consumeSessionSleepWake({
|
|
135
|
+
deliveryId: staleDeliveryId,
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
expect(staleClaim).toBe(null)
|
|
139
|
+
expect(staleConsume).toBe(false)
|
|
140
|
+
expect((await getSessionSleep({ sessionId: 'ses-generation' }))?.status).toBe(
|
|
141
|
+
'planned',
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('a wake becomes a turn at most once', async () => {
|
|
146
|
+
const deliveryId = await createSleep({
|
|
147
|
+
sessionId: 'ses-once',
|
|
148
|
+
threadId: 'thr-once',
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// A retry can post the same wake twice; only the first delivery may win.
|
|
152
|
+
const first = await consumeSessionSleepWake({ deliveryId })
|
|
153
|
+
const second = await consumeSessionSleepWake({ deliveryId })
|
|
154
|
+
|
|
155
|
+
expect(first).toBe(true)
|
|
156
|
+
expect(second).toBe(false)
|
|
157
|
+
expect((await getSessionSleep({ sessionId: 'ses-once' }))?.status).toBe(
|
|
158
|
+
'consumed',
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
test('a wake cancelled while posting is dropped instead of starting a turn', async () => {
|
|
163
|
+
const deliveryId = await createSleep({
|
|
164
|
+
sessionId: 'ses-raced',
|
|
165
|
+
threadId: 'thr-raced',
|
|
166
|
+
})
|
|
167
|
+
await claimSessionSleepAttempt({
|
|
168
|
+
sessionId: 'ses-raced',
|
|
169
|
+
deliveryId,
|
|
170
|
+
now: NOW,
|
|
171
|
+
retryAfterMs: RETRY_AFTER_MS,
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// User takes the conversation back while the post is in flight.
|
|
175
|
+
await cancelSessionSleepForThread({ threadId: 'thr-raced' })
|
|
176
|
+
|
|
177
|
+
expect(await consumeSessionSleepWake({ deliveryId })).toBe(false)
|
|
178
|
+
expect((await getSessionSleep({ sessionId: 'ses-raced' }))?.status).toBe(
|
|
179
|
+
'cancelled',
|
|
180
|
+
)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('cancel reaches the sleep from any thread bound to the session', async () => {
|
|
184
|
+
const deliveryId = await createSleep({
|
|
185
|
+
sessionId: 'ses-rebound',
|
|
186
|
+
threadId: 'thr-rebound-old',
|
|
187
|
+
})
|
|
188
|
+
// /resume binds the same session to a second thread. A sleep belongs to the
|
|
189
|
+
// session, not to the thread it was created in, so either thread cancels it.
|
|
190
|
+
await setThreadSession('thr-rebound-new', 'ses-rebound')
|
|
191
|
+
|
|
192
|
+
const cancelled = await cancelSessionSleepForThread({
|
|
193
|
+
threadId: 'thr-rebound-new',
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
expect(cancelled).toBe(true)
|
|
197
|
+
expect(await consumeSessionSleepWake({ deliveryId })).toBe(false)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('a wake nobody consumed stays due and is retried', async () => {
|
|
201
|
+
const deliveryId = await createSleep({
|
|
202
|
+
sessionId: 'ses-stale',
|
|
203
|
+
threadId: 'thr-stale',
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Posting does not change the status, so a wake that was delivered to
|
|
207
|
+
// Discord but never reached ingress is simply due again after the retry
|
|
208
|
+
// window. This is the same path that covers a crash before posting.
|
|
209
|
+
await claimSessionSleepAttempt({
|
|
210
|
+
sessionId: 'ses-stale',
|
|
211
|
+
deliveryId,
|
|
212
|
+
now: NOW,
|
|
213
|
+
retryAfterMs: RETRY_AFTER_MS,
|
|
214
|
+
})
|
|
215
|
+
expect((await getSessionSleep({ sessionId: 'ses-stale' }))?.status).toBe(
|
|
216
|
+
'planned',
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
const retryDue = await getDueSessionSleeps({
|
|
220
|
+
now: new Date(NOW.getTime() + RETRY_AFTER_MS + 1),
|
|
221
|
+
retryAfterMs: RETRY_AFTER_MS,
|
|
222
|
+
limit: 50,
|
|
223
|
+
})
|
|
224
|
+
expect(retryDue.map((row) => row.session_id)).toContain('ses-stale')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('failing a delivery is also guarded by delivery_id', async () => {
|
|
228
|
+
const deliveryId = await createSleep({
|
|
229
|
+
sessionId: 'ses-failed',
|
|
230
|
+
threadId: 'thr-failed',
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
expect(
|
|
234
|
+
await markSessionSleepFailed({
|
|
235
|
+
sessionId: 'ses-failed',
|
|
236
|
+
deliveryId: 'some-other-delivery',
|
|
237
|
+
}),
|
|
238
|
+
).toBe(false)
|
|
239
|
+
expect(
|
|
240
|
+
await markSessionSleepFailed({ sessionId: 'ses-failed', deliveryId }),
|
|
241
|
+
).toBe(true)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test('only planned rows that are due and not just attempted are returned', async () => {
|
|
245
|
+
await createSleep({ sessionId: 'ses-due', threadId: 'thr-due' })
|
|
246
|
+
await createSleep({
|
|
247
|
+
sessionId: 'ses-future',
|
|
248
|
+
threadId: 'thr-future',
|
|
249
|
+
wakeAt: new Date('2026-08-19T13:00:00Z'),
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
const due = await getDueSessionSleeps({
|
|
253
|
+
now: NOW,
|
|
254
|
+
retryAfterMs: RETRY_AFTER_MS,
|
|
255
|
+
limit: 50,
|
|
256
|
+
})
|
|
257
|
+
const ids = due.map((row) => row.session_id)
|
|
258
|
+
|
|
259
|
+
expect(ids).toContain('ses-due')
|
|
260
|
+
expect(ids).not.toContain('ses-future')
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
describe('session to thread resolution', () => {
|
|
265
|
+
test('resolves to the most recently bound thread, not an arbitrary one', async () => {
|
|
266
|
+
// Bindings are ordered by wall-clock time, so space them enough to get
|
|
267
|
+
// distinct millisecond stamps. Real /resume flows are seconds apart.
|
|
268
|
+
const tick = async () => {
|
|
269
|
+
await new Promise((resolve) => {
|
|
270
|
+
setTimeout(resolve, 5)
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await setThreadSession('thr-first', 'ses-resume')
|
|
275
|
+
await tick()
|
|
276
|
+
// /resume maps the same session into a brand new thread without clearing
|
|
277
|
+
// the old row. The newest binding must win, otherwise plugin tools post
|
|
278
|
+
// into a dead conversation.
|
|
279
|
+
await setThreadSession('thr-second', 'ses-resume')
|
|
280
|
+
|
|
281
|
+
expect(await getThreadIdBySessionId('ses-resume')).toBe('thr-second')
|
|
282
|
+
|
|
283
|
+
await tick()
|
|
284
|
+
// Rebinding the older thread makes it current again. created_at ordering
|
|
285
|
+
// would still answer thr-second here, which is why updated_at exists.
|
|
286
|
+
await setThreadSession('thr-first', 'ses-resume')
|
|
287
|
+
|
|
288
|
+
expect(await getThreadIdBySessionId('ses-resume')).toBe('thr-first')
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
afterAll(async () => {
|
|
293
|
+
await closeDb()
|
|
294
|
+
})
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import { describe, test, expect } from 'vitest'
|
|
6
6
|
import {
|
|
7
7
|
deriveThreadNameFromSessionTitle,
|
|
8
|
-
deriveThreadRenameFromSessionUpdate,
|
|
9
8
|
} from './session-handler/thread-session-runtime.js'
|
|
10
9
|
|
|
11
10
|
describe('deriveThreadNameFromSessionTitle', () => {
|
|
@@ -98,6 +97,42 @@ describe('deriveThreadNameFromSessionTitle', () => {
|
|
|
98
97
|
).toMatchInlineSnapshot(`undefined`)
|
|
99
98
|
})
|
|
100
99
|
|
|
100
|
+
test('ignores a title containing only OpenCode callout markup', () => {
|
|
101
|
+
expect(
|
|
102
|
+
deriveThreadNameFromSessionTitle({
|
|
103
|
+
sessionTitle: '<callout accent="#ef4444">',
|
|
104
|
+
currentName: 'Useful existing title',
|
|
105
|
+
}),
|
|
106
|
+
).toMatchInlineSnapshot(`undefined`)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('ignores paired OpenCode callout markup with no title text', () => {
|
|
110
|
+
expect(
|
|
111
|
+
deriveThreadNameFromSessionTitle({
|
|
112
|
+
sessionTitle: '<callout accent="#ef4444"></callout>',
|
|
113
|
+
currentName: 'Useful existing title',
|
|
114
|
+
}),
|
|
115
|
+
).toMatchInlineSnapshot(`undefined`)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('keeps useful text while removing OpenCode callout wrappers', () => {
|
|
119
|
+
expect(
|
|
120
|
+
deriveThreadNameFromSessionTitle({
|
|
121
|
+
sessionTitle: '<callout accent="#ef4444">Useful title</callout>',
|
|
122
|
+
currentName: 'seed',
|
|
123
|
+
}),
|
|
124
|
+
).toMatchInlineSnapshot(`"Useful title"`)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('preserves a thread prefix around a multiline uppercase callout', () => {
|
|
128
|
+
expect(
|
|
129
|
+
deriveThreadNameFromSessionTitle({
|
|
130
|
+
sessionTitle: '<CALLOUT icon="info">\nUseful title\n</CALLOUT>',
|
|
131
|
+
currentName: 'btw: original title',
|
|
132
|
+
}),
|
|
133
|
+
).toMatchInlineSnapshot(`"btw: Useful title"`)
|
|
134
|
+
})
|
|
135
|
+
|
|
101
136
|
test('preserves btw: prefix from current name', () => {
|
|
102
137
|
expect(
|
|
103
138
|
deriveThreadNameFromSessionTitle({
|
|
@@ -116,65 +151,36 @@ describe('deriveThreadNameFromSessionTitle', () => {
|
|
|
116
151
|
).toMatchInlineSnapshot(`"Fork: Forked task title"`)
|
|
117
152
|
})
|
|
118
153
|
|
|
119
|
-
test('
|
|
154
|
+
test('does not double a worktree prefix copied from the Discord thread name', () => {
|
|
120
155
|
expect(
|
|
121
156
|
deriveThreadNameFromSessionTitle({
|
|
122
|
-
sessionTitle:
|
|
123
|
-
currentName: '
|
|
124
|
-
}),
|
|
125
|
-
).toMatchInlineSnapshot(`undefined`)
|
|
126
|
-
expect(
|
|
127
|
-
deriveThreadNameFromSessionTitle({
|
|
128
|
-
sessionTitle: undefined,
|
|
129
|
-
currentName: 'seed',
|
|
157
|
+
sessionTitle: '⬦ Fix queue draining',
|
|
158
|
+
currentName: '⬦ Old name',
|
|
130
159
|
}),
|
|
131
|
-
).toMatchInlineSnapshot(`
|
|
160
|
+
).toMatchInlineSnapshot(`"⬦ Fix queue draining"`)
|
|
132
161
|
})
|
|
133
|
-
})
|
|
134
162
|
|
|
135
|
-
|
|
136
|
-
test('skips auto-rename after the thread differs from the persisted synced name', () => {
|
|
163
|
+
test('preserves the worktree prefix', () => {
|
|
137
164
|
expect(
|
|
138
|
-
|
|
139
|
-
sessionTitle: '
|
|
140
|
-
currentName: '
|
|
141
|
-
lastSyncedName: 'Old OpenCode title',
|
|
165
|
+
deriveThreadNameFromSessionTitle({
|
|
166
|
+
sessionTitle: 'Refactor queue',
|
|
167
|
+
currentName: '⬦ old worktree title',
|
|
142
168
|
}),
|
|
143
|
-
).toMatchInlineSnapshot(`
|
|
144
|
-
{
|
|
145
|
-
"desiredName": null,
|
|
146
|
-
"nextSyncedName": "Old OpenCode title",
|
|
147
|
-
}
|
|
148
|
-
`)
|
|
169
|
+
).toMatchInlineSnapshot(`"⬦ Refactor queue"`)
|
|
149
170
|
})
|
|
150
171
|
|
|
151
|
-
test('returns
|
|
172
|
+
test('returns undefined for null/undefined title', () => {
|
|
152
173
|
expect(
|
|
153
|
-
|
|
154
|
-
sessionTitle:
|
|
155
|
-
currentName: '
|
|
156
|
-
lastSyncedName: 'Old OpenCode title',
|
|
174
|
+
deriveThreadNameFromSessionTitle({
|
|
175
|
+
sessionTitle: null,
|
|
176
|
+
currentName: 'seed',
|
|
157
177
|
}),
|
|
158
|
-
).toMatchInlineSnapshot(`
|
|
159
|
-
{
|
|
160
|
-
"desiredName": "New OpenCode title",
|
|
161
|
-
"nextSyncedName": "New OpenCode title",
|
|
162
|
-
}
|
|
163
|
-
`)
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
test('remembers a no-op matching title as synced for later manual rename detection', () => {
|
|
178
|
+
).toMatchInlineSnapshot(`undefined`)
|
|
167
179
|
expect(
|
|
168
|
-
|
|
169
|
-
sessionTitle:
|
|
170
|
-
currentName: '
|
|
171
|
-
lastSyncedName: null,
|
|
180
|
+
deriveThreadNameFromSessionTitle({
|
|
181
|
+
sessionTitle: undefined,
|
|
182
|
+
currentName: 'seed',
|
|
172
183
|
}),
|
|
173
|
-
).toMatchInlineSnapshot(`
|
|
174
|
-
{
|
|
175
|
-
"desiredName": null,
|
|
176
|
-
"nextSyncedName": "Old OpenCode title",
|
|
177
|
-
}
|
|
178
|
-
`)
|
|
184
|
+
).toMatchInlineSnapshot(`undefined`)
|
|
179
185
|
})
|
|
180
186
|
})
|