kimaki 0.27.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 +247 -64
- package/dist/anthropic-account-identity.js +8 -1
- package/dist/anthropic-auth-plugin.js +9 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +6 -1
- package/dist/cli-commands/session.js +139 -82
- package/dist/cli-commands/task.js +21 -14
- package/dist/cli-parsing.test.js +24 -1
- package/dist/cli-runner.js +16 -4
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +158 -24
- package/dist/commands/ask-question.js +2 -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/merge-worktree.js +13 -6
- package/dist/commands/model-variant.js +3 -2
- package/dist/commands/model.js +4 -3
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +28 -0
- package/dist/db.js +11 -0
- package/dist/db.test.js +100 -0
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +501 -474
- package/dist/discord-command-registration.js +16 -9
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +84 -0
- package/dist/discord-utils.test.js +135 -1
- package/dist/errors.js +5 -0
- 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/gateway-proxy.e2e.test.js +18 -18
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +12 -7
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +2 -2
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/logger.js +1 -0
- package/dist/message-formatting.js +163 -44
- package/dist/message-formatting.test.js +473 -13
- package/dist/message-preprocessing.js +105 -1
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +49 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +29 -3
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +5 -5
- 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-footer.e2e.test.js +29 -32
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +9 -7
- package/dist/queue-advanced-question.e2e.test.js +2 -2
- 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 +20 -20
- package/dist/queue-question-select-drain.e2e.test.js +7 -7
- package/dist/runtime-lifecycle.e2e.test.js +16 -19
- package/dist/schema.js +1 -1
- package/dist/session-handler/event-stream-state.js +124 -0
- package/dist/session-handler/event-stream-state.test.js +322 -1
- package/dist/session-handler/model-utils.js +16 -3
- package/dist/session-handler/model-utils.test.js +103 -3
- 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 +340 -135
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-title-rename.test.js +30 -0
- package/dist/sleep.e2e.test.js +14 -11
- package/dist/system-message.js +45 -12
- package/dist/system-message.test.js +117 -10
- package/dist/task-runner.js +2 -1
- package/dist/task-schedule.js +32 -2
- package/dist/task-schedule.test.js +79 -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 -54
- package/dist/undo-redo.e2e.test.js +17 -9
- 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 +41 -1
- package/dist/worktree-lifecycle.e2e.test.js +30 -30
- package/package.json +8 -8
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +56 -232
- package/skills/holocron/SKILL.md +117 -17
- package/src/agent-model.e2e.test.ts +293 -66
- package/src/anthropic-account-identity.ts +19 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +11 -16
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +6 -1
- package/src/cli-commands/session.ts +171 -105
- package/src/cli-commands/task.ts +22 -15
- package/src/cli-parsing.test.ts +34 -1
- package/src/cli-runner.ts +16 -3
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +199 -23
- package/src/commands/ask-question.ts +2 -1
- 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/merge-worktree.ts +13 -6
- package/src/commands/model-variant.ts +3 -2
- package/src/commands/model.ts +4 -3
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +40 -0
- package/src/db.test.ts +105 -0
- package/src/db.ts +12 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +43 -7
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +27 -11
- package/src/discord-utils.test.ts +157 -0
- package/src/discord-utils.ts +131 -1
- package/src/errors.ts +7 -0
- 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/gateway-proxy.e2e.test.ts +18 -17
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +16 -9
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +5 -5
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/logger.ts +1 -0
- package/src/message-formatting.test.ts +586 -13
- package/src/message-formatting.ts +223 -46
- package/src/message-preprocessing.ts +116 -1
- package/src/opencode-command.test.ts +59 -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 +44 -4
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +5 -5
- 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-footer.e2e.test.ts +30 -31
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +9 -7
- package/src/queue-advanced-question.e2e.test.ts +2 -2
- 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 +20 -20
- package/src/queue-question-select-drain.e2e.test.ts +7 -7
- package/src/runtime-lifecycle.e2e.test.ts +16 -18
- package/src/schema.sql +1 -1
- package/src/schema.ts +1 -1
- package/src/session-handler/event-stream-state.test.ts +353 -0
- package/src/session-handler/event-stream-state.ts +179 -0
- package/src/session-handler/model-utils.test.ts +125 -3
- package/src/session-handler/model-utils.ts +36 -4
- 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 +427 -158
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-title-rename.test.ts +45 -0
- package/src/sleep.e2e.test.ts +14 -11
- package/src/store.ts +1 -0
- package/src/system-message.test.ts +141 -10
- package/src/system-message.ts +66 -11
- package/src/task-runner.ts +2 -1
- package/src/task-schedule.test.ts +94 -0
- package/src/task-schedule.ts +47 -2
- 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 -53
- package/src/undo-redo.e2e.test.ts +17 -9
- 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 +44 -0
- package/src/voice.ts +34 -6
- package/src/worktree-lifecycle.e2e.test.ts +30 -30
|
@@ -25,6 +25,15 @@ afterEach(async () => {
|
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
describe('system-message', () => {
|
|
28
|
+
test('requires kimaki upload for Discord images, not markdown', () => {
|
|
29
|
+
const message = getOpencodeSystemMessage({
|
|
30
|
+
sessionId: 'ses_123',
|
|
31
|
+
})
|
|
32
|
+
expect(message).toContain('NEVER show images with markdown')
|
|
33
|
+
expect(message).toContain('Discord does not render local markdown images')
|
|
34
|
+
expect(message).toContain('ALWAYS upload them with `kimaki upload-to-discord`')
|
|
35
|
+
})
|
|
36
|
+
|
|
28
37
|
test('requires reading the report-bugs guide before filing kimaki issues', () => {
|
|
29
38
|
const message = getOpencodeSystemMessage({
|
|
30
39
|
sessionId: 'ses_123',
|
|
@@ -48,6 +57,30 @@ describe('system-message', () => {
|
|
|
48
57
|
expect(message).toContain('<callout accent="#f59e0b">')
|
|
49
58
|
})
|
|
50
59
|
|
|
60
|
+
test('tells the model the sleep tool result is not a wake', () => {
|
|
61
|
+
const message = getOpencodeSystemMessage({
|
|
62
|
+
sessionId: 'ses_123',
|
|
63
|
+
})
|
|
64
|
+
expect(message).toContain('The tool result is not a wake')
|
|
65
|
+
expect(message).toContain('Woke after sleeping until')
|
|
66
|
+
expect(message).toContain(
|
|
67
|
+
'If you still need to wake later after answering, call `kimaki_sleep` again',
|
|
68
|
+
)
|
|
69
|
+
expect(message).not.toContain('After wake, continue the wait reason')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('tells the model to stay quiet between tool calls', () => {
|
|
73
|
+
const message = getOpencodeSystemMessage({
|
|
74
|
+
sessionId: 'ses_123',
|
|
75
|
+
})
|
|
76
|
+
expect(message).toContain('## Discord output')
|
|
77
|
+
expect(message).toContain('Be concise')
|
|
78
|
+
expect(message).toContain('Do not narrate between tool calls')
|
|
79
|
+
expect(message).toContain(
|
|
80
|
+
'Do not output text until you are ready to give the user the final answer for this turn',
|
|
81
|
+
)
|
|
82
|
+
})
|
|
83
|
+
|
|
51
84
|
test('requires interactive tools after all text, using exact tool names', () => {
|
|
52
85
|
const message = getOpencodeSystemMessage({
|
|
53
86
|
sessionId: 'ses_123',
|
|
@@ -110,6 +143,18 @@ describe('system-message', () => {
|
|
|
110
143
|
).rejects.toMatchObject({ code: 'EISDIR' })
|
|
111
144
|
})
|
|
112
145
|
|
|
146
|
+
test('includes all-projects session search example', () => {
|
|
147
|
+
const message = getOpencodeSystemMessage({
|
|
148
|
+
sessionId: 'ses_123',
|
|
149
|
+
channelId: 'chan_123',
|
|
150
|
+
})
|
|
151
|
+
expect(message).toContain('kimaki session search "auth timeout" --all')
|
|
152
|
+
expect(message).toContain('kimaki session search "auth timeout" --days 0')
|
|
153
|
+
expect(message).toContain(
|
|
154
|
+
'Defaults to this project and the last 14 days. Use `--days 0` for all time. Use `--all` to search every locally registered project',
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
|
|
113
158
|
test('includes session title update guidance when scope or goal changed', () => {
|
|
114
159
|
const message = getOpencodeSystemMessage({
|
|
115
160
|
sessionId: 'ses_123',
|
|
@@ -142,6 +187,78 @@ describe('system-message', () => {
|
|
|
142
187
|
expect(message).toContain('--parent-session ses_child')
|
|
143
188
|
})
|
|
144
189
|
|
|
190
|
+
test('scheduled cron task section shows task id, cron and no-sleep rule', () => {
|
|
191
|
+
const message = getOpencodeSystemMessage({
|
|
192
|
+
sessionId: 'ses_task',
|
|
193
|
+
channelId: 'chan_123',
|
|
194
|
+
threadId: 'thread_123',
|
|
195
|
+
scheduledTask: {
|
|
196
|
+
taskId: 23,
|
|
197
|
+
scheduleKind: 'cron',
|
|
198
|
+
cronExpr: '0 6,14 * * *',
|
|
199
|
+
timezone: 'Europe/Rome',
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
expect(message).toContain('## scheduled task session')
|
|
203
|
+
expect(message).toContain('kimaki scheduled task #23')
|
|
204
|
+
expect(message).toContain('Schedule: cron `0 6,14 * * *` in Europe/Rome.')
|
|
205
|
+
expect(message).toContain('Do NOT use `kimaki_sleep` to wait for the next run')
|
|
206
|
+
expect(message).toContain('starts a fresh session automatically')
|
|
207
|
+
const section = message.slice(
|
|
208
|
+
message.indexOf('## scheduled task session'),
|
|
209
|
+
message.indexOf('## archiving the current thread'),
|
|
210
|
+
)
|
|
211
|
+
expect(section).not.toContain('archive')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('cron task without timezone says UTC', () => {
|
|
215
|
+
const message = getOpencodeSystemMessage({
|
|
216
|
+
sessionId: 'ses_task_utc',
|
|
217
|
+
scheduledTask: { taskId: 7, scheduleKind: 'cron', cronExpr: '0 9 * * 1' },
|
|
218
|
+
})
|
|
219
|
+
expect(message).toContain('Schedule: cron `0 9 * * 1` in UTC.')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
test('one-shot at task says it does not repeat and skips sleep', () => {
|
|
223
|
+
const message = getOpencodeSystemMessage({
|
|
224
|
+
sessionId: 'ses_oneshot',
|
|
225
|
+
scheduledTask: { scheduleKind: 'at' },
|
|
226
|
+
})
|
|
227
|
+
expect(message).toContain('## scheduled task session')
|
|
228
|
+
expect(message).toContain('a one-time kimaki scheduled task')
|
|
229
|
+
expect(message).toContain('This task runs once and does not repeat.')
|
|
230
|
+
expect(message).toContain('Do NOT use `kimaki_sleep` to wait for the next run')
|
|
231
|
+
expect(message).not.toContain('Schedule: cron')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('no scheduled task section by default (normal sessions)', () => {
|
|
235
|
+
const message = getOpencodeSystemMessage({
|
|
236
|
+
sessionId: 'ses_normal',
|
|
237
|
+
channelId: 'chan_123',
|
|
238
|
+
threadId: 'thread_123',
|
|
239
|
+
})
|
|
240
|
+
expect(message).not.toContain('## scheduled task session')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('lets agents omit --user for quiet scheduled work', () => {
|
|
244
|
+
const message = getOpencodeSystemMessage({
|
|
245
|
+
sessionId: 'ses_123',
|
|
246
|
+
channelId: 'chan_123',
|
|
247
|
+
threadId: 'thread_123',
|
|
248
|
+
})
|
|
249
|
+
expect(message).not.toContain(
|
|
250
|
+
'ALWAYS pass `--user` when scheduling a task.',
|
|
251
|
+
)
|
|
252
|
+
expect(message).not.toContain('(always pass this)')
|
|
253
|
+
expect(message).toContain("kimaki task edit <id> --user ''")
|
|
254
|
+
expect(message).toContain(
|
|
255
|
+
"kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123",
|
|
256
|
+
)
|
|
257
|
+
expect(message).not.toContain(
|
|
258
|
+
"kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'",
|
|
259
|
+
)
|
|
260
|
+
})
|
|
261
|
+
|
|
145
262
|
test('omits parent session system block by default (btw/task/fork cache)', () => {
|
|
146
263
|
// /btw, task subagents, and normal sessions must not get a parent block in
|
|
147
264
|
// the system message. That block is opt-in via --parent-session only.
|
|
@@ -185,6 +302,12 @@ describe('system-message', () => {
|
|
|
185
302
|
"
|
|
186
303
|
The user is reading your messages from inside Discord, via kimaki.dev
|
|
187
304
|
|
|
305
|
+
## Discord output
|
|
306
|
+
|
|
307
|
+
Be concise. Do not narrate between tool calls. Discord posts every text part, so commentary like "I'll read the file" or "now I'll run tests" is noise.
|
|
308
|
+
Do not output text until you are ready to give the user the final answer for this turn. Tool calls can run with no preceding text.
|
|
309
|
+
Exceptions: when a tool requires user-visible text first (\`question\`, \`kimaki_action_buttons\`, \`kimaki_file_upload\`, \`kimaki_sleep\`), write that required text, then call the tool.
|
|
310
|
+
|
|
188
311
|
## bash tool
|
|
189
312
|
|
|
190
313
|
When calling the bash tool, always include these extra fields alongside \`command\`:
|
|
@@ -201,7 +324,7 @@ describe('system-message', () => {
|
|
|
201
324
|
}
|
|
202
325
|
\`\`\`
|
|
203
326
|
|
|
204
|
-
\`description\` is shown
|
|
327
|
+
\`description\` is shown in Discord when the bash command is longer than 50 characters.
|
|
205
328
|
\`hasSideEffect\` distinguishes essential bash calls from read-only ones in low-verbosity mode.
|
|
206
329
|
|
|
207
330
|
Your current OpenCode session ID is: ses_123
|
|
@@ -241,6 +364,8 @@ describe('system-message', () => {
|
|
|
241
364
|
|
|
242
365
|
kimaki upload-to-discord --session ses_123 <file1> [file2] ...
|
|
243
366
|
|
|
367
|
+
NEVER show images with markdown like \`\` or \`\`. Discord does not render local markdown images. ALWAYS upload them with \`kimaki upload-to-discord\` so they appear as real Discord attachments. Do this for every screenshot, generated image, and visual step the user should see.
|
|
368
|
+
|
|
244
369
|
## generating audio from text
|
|
245
370
|
|
|
246
371
|
When the user asks you to generate audio of some text so they can listen instead of reading, use \`kimaki tts\` to create a speech file and \`kimaki upload-to-discord\` to send it to the thread. Only use this when the user explicitly asks for audio.
|
|
@@ -267,7 +392,9 @@ describe('system-message', () => {
|
|
|
267
392
|
Use \`kimaki_sleep\` to pause this session for hours or days, then continue when the time is reached. The sleep is stored in SQLite and survives bot restarts.
|
|
268
393
|
Pass either \`duration\` (\`30s\`, \`2h\`, \`1d\`) or \`until\` (UTC ISO ending with \`Z\`, example \`2026-08-20T09:00:00Z\`).
|
|
269
394
|
You MUST call \`kimaki_sleep\` LAST, after ALL text. Do not call more tools after it.
|
|
270
|
-
A new user message cancels the sleep.
|
|
395
|
+
A new user message cancels the sleep. If you still need to wake later after answering, call \`kimaki_sleep\` again with \`until\` set to the original UTC time.
|
|
396
|
+
The tool result is not a wake. After it succeeds, write one short line that you are waiting, then stop. Do not continue the wait reason and do not pretend time has passed.
|
|
397
|
+
Wake is a later Discord message that starts with \`Woke after sleeping until\`. Only then continue the wait reason.
|
|
271
398
|
|
|
272
399
|
## archiving the current thread
|
|
273
400
|
|
|
@@ -397,13 +524,13 @@ describe('system-message', () => {
|
|
|
397
524
|
Use \`--send-at\` to schedule a one-time or recurring task:
|
|
398
525
|
|
|
399
526
|
kimaki send --channel chan_123 --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent> --parent-session ses_123 --user '<discord-user-id>'
|
|
400
|
-
kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123
|
|
527
|
+
kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123
|
|
401
528
|
|
|
402
529
|
Use \`--pre-run '<command>'\` to check whether a scheduled task should start. Kimaki runs the command in the project directory. Exit code 0 starts the session and appends stdout to the prompt. Any other exit code skips that occurrence. Command output is written to the Kimaki log.
|
|
403
530
|
|
|
404
531
|
Scheduled tasks do not overlap by default. Add \`--allow-concurrency\` only when concurrent sessions from the same task are safe.
|
|
405
532
|
|
|
406
|
-
|
|
533
|
+
Pass \`--user\` when the user should see or act on the thread. Omit \`--user\` for routine autonomous work (digests, monitors, housekeeping) so the thread does not appear in the user's Discord sidebar. Discord only shows a thread in the left sidebar to its members. Without \`--user\`, kimaki does not ensure anyone is a member. This applies to \`--channel\` and \`--thread\` scheduling alike.
|
|
407
534
|
|
|
408
535
|
ALL scheduling is in UTC. Dates must be UTC ISO format ending with \`Z\`. Cron expressions also fire in UTC (e.g. \`0 9 * * 1\` means 9:00 UTC every Monday).
|
|
409
536
|
When the user specifies a time without a timezone, ask them to confirm their timezone or the UTC equivalent. Never guess the user's timezone.
|
|
@@ -415,14 +542,14 @@ describe('system-message', () => {
|
|
|
415
542
|
- \`--pre-run\` to start only when a project command exits with code 0
|
|
416
543
|
- \`--allow-concurrency\` to permit overlapping runs from the same task
|
|
417
544
|
- \`--parent-session\` to pass this session as parent of the scheduled child
|
|
418
|
-
- \`--user\` to add a specific user to the scheduled thread
|
|
545
|
+
- \`--user\` to add a specific user to the scheduled thread. Omit this for quiet autonomous work
|
|
419
546
|
|
|
420
547
|
\`--wait\` is incompatible with \`--send-at\` because scheduled tasks run in the future.
|
|
421
548
|
|
|
422
549
|
Keep scheduled task prompts **short**. The prompt text becomes the first message in the Discord thread, so long prompts clutter the channel. Instead of inlining the full task description in \`--prompt\`, write a markdown file in the project's \`tasks/\` folder and reference it:
|
|
423
550
|
|
|
424
551
|
\`\`\`bash
|
|
425
|
-
kimaki send --channel chan_123 --prompt 'Read tasks/weekly-test-suite.md and follow instructions' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123
|
|
552
|
+
kimaki send --channel chan_123 --prompt 'Read tasks/weekly-test-suite.md and follow instructions' --send-at '0 9 * * 1' --agent <current_agent> --parent-session ses_123
|
|
426
553
|
\`\`\`
|
|
427
554
|
|
|
428
555
|
The task file should contain all the detail: goal, constraints, expected output, completion criteria. Use this frontmatter format:
|
|
@@ -441,7 +568,7 @@ describe('system-message', () => {
|
|
|
441
568
|
Notification strategy:
|
|
442
569
|
- NEVER use \`@username\` (e.g. \`@Tommy\`) directly in task prompts. The prompt text becomes the first message in the thread, so a raw \`@\` mention triggers an actual Discord ping every time the task fires. Instead, wrap it in inline code like \`\\\`@Tommy\\\`\`, or use Discord user ID mentions like \`<@USER_ID>\` only in the body of the prompt where the agent will process it, not in the opening line.
|
|
443
570
|
- If a task needs user attention, add "mention the user via Discord user ID when task requires user review" in the task md file.
|
|
444
|
-
- With \`--user\`, the user is added to the thread and receives thread-level notifications.
|
|
571
|
+
- With \`--user\`, the user is added to the thread and receives thread-level notifications. Omit \`--user\` when the work should stay out of the sidebar.
|
|
445
572
|
- If a scheduled task completes with no actionable result, archive the session: \`kimaki session archive thread_123 (or --session ses_123)\`
|
|
446
573
|
|
|
447
574
|
Manage scheduled tasks with:
|
|
@@ -450,7 +577,7 @@ describe('system-message', () => {
|
|
|
450
577
|
kimaki task edit <id> --prompt "new prompt" [--send-at "new schedule"] [--pre-run "command"] [--allow-concurrency true|false] [--user "<discord-user-id>"] [--model "provider/model"] [--agent "<agent>"]
|
|
451
578
|
kimaki task delete <id>
|
|
452
579
|
|
|
453
|
-
\`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it.
|
|
580
|
+
\`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it. Add a user with \`kimaki task edit <id> --user '<discord-user-id>'\`. Clear a stored user with \`kimaki task edit <id> --user ''\`. Change model or agent in place with \`--model\` / \`--agent\` (empty string clears the override). Do not read SQLite or recreate the task just to swap model.
|
|
454
581
|
|
|
455
582
|
\`kimaki session list\` also shows if a session was started by a scheduled \`delay\` or \`cron\` task, including task ID when available.
|
|
456
583
|
|
|
@@ -533,13 +660,17 @@ describe('system-message', () => {
|
|
|
533
660
|
kimaki session list --active
|
|
534
661
|
\`\`\`
|
|
535
662
|
|
|
536
|
-
|
|
663
|
+
Titles prefixed with \`btw:\` are side sessions that answer a related user question in parallel. They are not duplicate sessions of the main task.
|
|
664
|
+
|
|
665
|
+
To search past sessions (supports plain text or /regex/flags). Defaults to this project and the last 14 days. Use \`--days 0\` for all time. Use \`--all\` to search every locally registered project:
|
|
537
666
|
|
|
538
667
|
\`\`\`bash
|
|
539
668
|
kimaki session search "auth timeout"
|
|
669
|
+
kimaki session search "auth timeout" --days 0
|
|
540
670
|
kimaki session search "/error\\s+42/i"
|
|
541
671
|
kimaki session search "rate limit" --project /path/to/project
|
|
542
672
|
kimaki session search "/panic|crash/i" --channel <channel_id>
|
|
673
|
+
kimaki session search "auth timeout" --all
|
|
543
674
|
\`\`\`
|
|
544
675
|
|
|
545
676
|
To read a session's full conversation as markdown, pipe to a file and grep it to avoid wasting context.
|
|
@@ -749,7 +880,7 @@ describe('system-message', () => {
|
|
|
749
880
|
|
|
750
881
|
## running dev servers with tunnel access
|
|
751
882
|
|
|
752
|
-
|
|
883
|
+
When starting a local dev server that the Discord user should open in a browser, prefer wrapping it in \`kimaki tunnel\` so they get a public URL. Localhost URLs are useless from Discord. Invoke Kimaki directly as \`kimaki\`, not via \`npx\` or \`bunx\`.
|
|
753
884
|
|
|
754
885
|
Use \`bunx tuistory\` to run the tunnel + dev server combo in the background so it persists across commands. This is preferable to raw shell backgrounding because you can wait for real output, read logs, and interact with the running process.
|
|
755
886
|
|
package/src/system-message.ts
CHANGED
|
@@ -13,6 +13,7 @@ import fs from 'node:fs'
|
|
|
13
13
|
import path from 'node:path'
|
|
14
14
|
import { getDataDir } from './config.js'
|
|
15
15
|
import { store } from './store.js'
|
|
16
|
+
import { SESSION_SEARCH_DEFAULT_DAYS } from './session-search.js'
|
|
16
17
|
|
|
17
18
|
/** Subfolder under the kimaki data dir for session system prompt side-channel files. */
|
|
18
19
|
export const SESSION_SYSTEM_PROMPT_DIR = 'session-system'
|
|
@@ -175,7 +176,7 @@ this feature by restarting kimaki with the \`--no-critique\` flag.
|
|
|
175
176
|
const KIMAKI_TUNNEL_INSTRUCTIONS = `
|
|
176
177
|
## running dev servers with tunnel access
|
|
177
178
|
|
|
178
|
-
|
|
179
|
+
When starting a local dev server that the Discord user should open in a browser, prefer wrapping it in \`kimaki tunnel\` so they get a public URL. Localhost URLs are useless from Discord. Invoke Kimaki directly as \`kimaki\`, not via \`npx\` or \`bunx\`.
|
|
179
180
|
|
|
180
181
|
Use \`bunx tuistory\` to run the tunnel + dev server combo in the background so it persists across commands. This is preferable to raw shell backgrounding because you can wait for real output, read logs, and interact with the running process.
|
|
181
182
|
|
|
@@ -341,6 +342,40 @@ export type AgentInfo = {
|
|
|
341
342
|
description?: string
|
|
342
343
|
}
|
|
343
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Info about the scheduled task that started this session, resolved once per
|
|
347
|
+
* session and kept stable across turns so the system prompt prefix stays
|
|
348
|
+
* cacheable.
|
|
349
|
+
*/
|
|
350
|
+
export type ScheduledTaskSystemContext = {
|
|
351
|
+
/** Scheduled task ID. Missing for one-shot 'at' tasks (deleted after run). */
|
|
352
|
+
taskId?: number
|
|
353
|
+
scheduleKind: 'at' | 'cron'
|
|
354
|
+
/** Cron expression in the task's timezone. Only set for 'cron' tasks. */
|
|
355
|
+
cronExpr?: string | null
|
|
356
|
+
/** IANA timezone the cron fires in. Defaults to UTC when unset. */
|
|
357
|
+
timezone?: string | null
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function getScheduledTaskSection(context: ScheduledTaskSystemContext): string {
|
|
361
|
+
const origin = context.taskId
|
|
362
|
+
? `kimaki scheduled task #${context.taskId}`
|
|
363
|
+
: 'a one-time kimaki scheduled task'
|
|
364
|
+
const schedule = context.scheduleKind === 'cron' && context.cronExpr
|
|
365
|
+
? `Schedule: cron \`${context.cronExpr}\`${context.timezone ? ` in ${context.timezone}` : ' in UTC'}.`
|
|
366
|
+
: context.scheduleKind === 'at'
|
|
367
|
+
? 'This task runs once and does not repeat.'
|
|
368
|
+
: ''
|
|
369
|
+
return `
|
|
370
|
+
## scheduled task session
|
|
371
|
+
|
|
372
|
+
This session was started automatically by ${origin}.
|
|
373
|
+
${schedule}
|
|
374
|
+
When your run is done, just stop: the task fires again on its schedule and starts a fresh session automatically.
|
|
375
|
+
Do NOT use \`kimaki_sleep\` to wait for the next run. Sleeping pins this session and never triggers the next one; each firing of the task starts a new session on its own.
|
|
376
|
+
`
|
|
377
|
+
}
|
|
378
|
+
|
|
344
379
|
function escapePromptAttribute(value: string): string {
|
|
345
380
|
return value
|
|
346
381
|
.replaceAll('&', '&')
|
|
@@ -431,6 +466,7 @@ export function getOpencodeSystemMessage({
|
|
|
431
466
|
agents,
|
|
432
467
|
userId,
|
|
433
468
|
parentSessionId,
|
|
469
|
+
scheduledTask,
|
|
434
470
|
}: {
|
|
435
471
|
sessionId: string
|
|
436
472
|
channelId?: string
|
|
@@ -449,6 +485,11 @@ export function getOpencodeSystemMessage({
|
|
|
449
485
|
* per-parent block. Never auto-derive this from OpenCode parent session IDs.
|
|
450
486
|
*/
|
|
451
487
|
parentSessionId?: string
|
|
488
|
+
/**
|
|
489
|
+
* Set only when the session was started by a scheduled task. Resolved from
|
|
490
|
+
* the session_start_sources row, so it stays identical across turns.
|
|
491
|
+
*/
|
|
492
|
+
scheduledTask?: ScheduledTaskSystemContext
|
|
452
493
|
}) {
|
|
453
494
|
const userArg = ` --user '${userId || '<discord-user-id>'}'`
|
|
454
495
|
const parentSessionArg = ` --parent-session ${sessionId}`
|
|
@@ -474,6 +515,12 @@ export function getOpencodeSystemMessage({
|
|
|
474
515
|
return `
|
|
475
516
|
The user is reading your messages from inside Discord, via kimaki.dev
|
|
476
517
|
|
|
518
|
+
## Discord output
|
|
519
|
+
|
|
520
|
+
Be concise. Do not narrate between tool calls. Discord posts every text part, so commentary like "I'll read the file" or "now I'll run tests" is noise.
|
|
521
|
+
Do not output text until you are ready to give the user the final answer for this turn. Tool calls can run with no preceding text.
|
|
522
|
+
Exceptions: when a tool requires user-visible text first (\`question\`, \`kimaki_action_buttons\`, \`kimaki_file_upload\`, \`kimaki_sleep\`), write that required text, then call the tool.
|
|
523
|
+
|
|
477
524
|
## bash tool
|
|
478
525
|
|
|
479
526
|
When calling the bash tool, always include these extra fields alongside \`command\`:
|
|
@@ -490,7 +537,7 @@ interface BashToolInput {
|
|
|
490
537
|
}
|
|
491
538
|
\`\`\`
|
|
492
539
|
|
|
493
|
-
\`description\` is shown
|
|
540
|
+
\`description\` is shown in Discord when the bash command is longer than 50 characters.
|
|
494
541
|
\`hasSideEffect\` distinguishes essential bash calls from read-only ones in low-verbosity mode.
|
|
495
542
|
|
|
496
543
|
Your current OpenCode session ID is: ${sessionId}${channelId ? `\nYour current Discord channel ID is: ${channelId}` : ''}${threadId ? `\nYour current Discord thread ID is: ${threadId}` : ''}${guildId ? `\nYour current Discord guild ID is: ${guildId}` : ''}${parentSessionContext}
|
|
@@ -527,6 +574,8 @@ To upload files to the Discord thread (images, screenshots, long files that woul
|
|
|
527
574
|
|
|
528
575
|
kimaki upload-to-discord --session ${sessionId} <file1> [file2] ...
|
|
529
576
|
|
|
577
|
+
NEVER show images with markdown like \`\` or \`\`. Discord does not render local markdown images. ALWAYS upload them with \`kimaki upload-to-discord\` so they appear as real Discord attachments. Do this for every screenshot, generated image, and visual step the user should see.
|
|
578
|
+
|
|
530
579
|
## generating audio from text
|
|
531
580
|
|
|
532
581
|
When the user asks you to generate audio of some text so they can listen instead of reading, use \`kimaki tts\` to create a speech file and \`kimaki upload-to-discord\` to send it to the thread. Only use this when the user explicitly asks for audio.
|
|
@@ -553,8 +602,10 @@ You MUST call \`kimaki_file_upload\` LAST, after ALL text.
|
|
|
553
602
|
Use \`kimaki_sleep\` to pause this session for hours or days, then continue when the time is reached. The sleep is stored in SQLite and survives bot restarts.
|
|
554
603
|
Pass either \`duration\` (\`30s\`, \`2h\`, \`1d\`) or \`until\` (UTC ISO ending with \`Z\`, example \`2026-08-20T09:00:00Z\`).
|
|
555
604
|
You MUST call \`kimaki_sleep\` LAST, after ALL text. Do not call more tools after it.
|
|
556
|
-
A new user message cancels the sleep.
|
|
557
|
-
|
|
605
|
+
A new user message cancels the sleep. If you still need to wake later after answering, call \`kimaki_sleep\` again with \`until\` set to the original UTC time.
|
|
606
|
+
The tool result is not a wake. After it succeeds, write one short line that you are waiting, then stop. Do not continue the wait reason and do not pretend time has passed.
|
|
607
|
+
Wake is a later Discord message that starts with \`Woke after sleeping until\`. Only then continue the wait reason.
|
|
608
|
+
${scheduledTask ? getScheduledTaskSection(scheduledTask) : ''}
|
|
558
609
|
## archiving the current thread
|
|
559
610
|
|
|
560
611
|
To archive the current Discord thread (hide it from sidebar) without stopping the session, run:
|
|
@@ -681,13 +732,13 @@ kimaki send --thread <thread_id> --prompt '/<agentname>-agent' --agent <current_
|
|
|
681
732
|
Use \`--send-at\` to schedule a one-time or recurring task:
|
|
682
733
|
|
|
683
734
|
kimaki send --channel ${channelId} --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent>${parentSessionArg}${userArg}
|
|
684
|
-
kimaki send --channel ${channelId} --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}
|
|
735
|
+
kimaki send --channel ${channelId} --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}
|
|
685
736
|
|
|
686
737
|
Use \`--pre-run '<command>'\` to check whether a scheduled task should start. Kimaki runs the command in the project directory. Exit code 0 starts the session and appends stdout to the prompt. Any other exit code skips that occurrence. Command output is written to the Kimaki log.
|
|
687
738
|
|
|
688
739
|
Scheduled tasks do not overlap by default. Add \`--allow-concurrency\` only when concurrent sessions from the same task are safe.
|
|
689
740
|
|
|
690
|
-
|
|
741
|
+
Pass \`--user\` when the user should see or act on the thread. Omit \`--user\` for routine autonomous work (digests, monitors, housekeeping) so the thread does not appear in the user's Discord sidebar. Discord only shows a thread in the left sidebar to its members. Without \`--user\`, kimaki does not ensure anyone is a member. This applies to \`--channel\` and \`--thread\` scheduling alike.
|
|
691
742
|
|
|
692
743
|
ALL scheduling is in UTC. Dates must be UTC ISO format ending with \`Z\`. Cron expressions also fire in UTC (e.g. \`0 9 * * 1\` means 9:00 UTC every Monday).
|
|
693
744
|
When the user specifies a time without a timezone, ask them to confirm their timezone or the UTC equivalent. Never guess the user's timezone.
|
|
@@ -699,14 +750,14 @@ When the user specifies a time without a timezone, ask them to confirm their tim
|
|
|
699
750
|
- \`--pre-run\` to start only when a project command exits with code 0
|
|
700
751
|
- \`--allow-concurrency\` to permit overlapping runs from the same task
|
|
701
752
|
- \`--parent-session\` to pass this session as parent of the scheduled child
|
|
702
|
-
- \`--user\` to add a specific user to the scheduled thread
|
|
753
|
+
- \`--user\` to add a specific user to the scheduled thread. Omit this for quiet autonomous work
|
|
703
754
|
|
|
704
755
|
\`--wait\` is incompatible with \`--send-at\` because scheduled tasks run in the future.
|
|
705
756
|
|
|
706
757
|
Keep scheduled task prompts **short**. The prompt text becomes the first message in the Discord thread, so long prompts clutter the channel. Instead of inlining the full task description in \`--prompt\`, write a markdown file in the project's \`tasks/\` folder and reference it:
|
|
707
758
|
|
|
708
759
|
\`\`\`bash
|
|
709
|
-
kimaki send --channel ${channelId} --prompt 'Read tasks/weekly-test-suite.md and follow instructions' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}
|
|
760
|
+
kimaki send --channel ${channelId} --prompt 'Read tasks/weekly-test-suite.md and follow instructions' --send-at '0 9 * * 1' --agent <current_agent>${parentSessionArg}
|
|
710
761
|
\`\`\`
|
|
711
762
|
|
|
712
763
|
The task file should contain all the detail: goal, constraints, expected output, completion criteria. Use this frontmatter format:
|
|
@@ -725,7 +776,7 @@ For simple reminders and notifications (\`--notify-only\`), inline the prompt di
|
|
|
725
776
|
Notification strategy:
|
|
726
777
|
- NEVER use \`@username\` (e.g. \`@Tommy\`) directly in task prompts. The prompt text becomes the first message in the thread, so a raw \`@\` mention triggers an actual Discord ping every time the task fires. Instead, wrap it in inline code like \`\\\`@Tommy\\\`\`, or use Discord user ID mentions like \`<@USER_ID>\` only in the body of the prompt where the agent will process it, not in the opening line.
|
|
727
778
|
- If a task needs user attention, add "mention the user via Discord user ID when task requires user review" in the task md file.
|
|
728
|
-
- With \`--user\`, the user is added to the thread and receives thread-level notifications.
|
|
779
|
+
- With \`--user\`, the user is added to the thread and receives thread-level notifications. Omit \`--user\` when the work should stay out of the sidebar.
|
|
729
780
|
- If a scheduled task completes with no actionable result, archive the session: \`kimaki session archive ${archiveTarget}\`
|
|
730
781
|
|
|
731
782
|
Manage scheduled tasks with:
|
|
@@ -734,7 +785,7 @@ kimaki task list
|
|
|
734
785
|
kimaki task edit <id> --prompt "new prompt" [--send-at "new schedule"] [--pre-run "command"] [--allow-concurrency true|false] [--user "<discord-user-id>"] [--model "provider/model"] [--agent "<agent>"]
|
|
735
786
|
kimaki task delete <id>
|
|
736
787
|
|
|
737
|
-
\`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it.
|
|
788
|
+
\`kimaki task list\` prints \`userId\`, \`agent\`, and \`model\` columns. A \`-\` in \`userId\` means nobody is added to the thread when that task fires, so the user may never see it. Add a user with \`kimaki task edit <id> --user '<discord-user-id>'\`. Clear a stored user with \`kimaki task edit <id> --user ''\`. Change model or agent in place with \`--model\` / \`--agent\` (empty string clears the override). Do not read SQLite or recreate the task just to swap model.
|
|
738
789
|
|
|
739
790
|
\`kimaki session list\` also shows if a session was started by a scheduled \`delay\` or \`cron\` task, including task ID when available.
|
|
740
791
|
|
|
@@ -817,13 +868,17 @@ kimaki session list --project /path/to/project # specific project
|
|
|
817
868
|
kimaki session list --active
|
|
818
869
|
\`\`\`
|
|
819
870
|
|
|
820
|
-
|
|
871
|
+
Titles prefixed with \`btw:\` are side sessions that answer a related user question in parallel. They are not duplicate sessions of the main task.
|
|
872
|
+
|
|
873
|
+
To search past sessions (supports plain text or /regex/flags). Defaults to this project and the last ${SESSION_SEARCH_DEFAULT_DAYS} days. Use \`--days 0\` for all time. Use \`--all\` to search every locally registered project:
|
|
821
874
|
|
|
822
875
|
\`\`\`bash
|
|
823
876
|
kimaki session search "auth timeout"
|
|
877
|
+
kimaki session search "auth timeout" --days 0
|
|
824
878
|
kimaki session search "/error\\s+42/i"
|
|
825
879
|
kimaki session search "rate limit" --project /path/to/project
|
|
826
880
|
kimaki session search "/panic|crash/i" --channel <channel_id>
|
|
881
|
+
kimaki session search "auth timeout" --all
|
|
827
882
|
\`\`\`
|
|
828
883
|
|
|
829
884
|
To read a session's full conversation as markdown, pipe to a file and grep it to avoid wasting context.
|
package/src/task-runner.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
markSessionSleepFailed,
|
|
28
28
|
} from './database.js'
|
|
29
29
|
import { execAsync } from './exec-async.js'
|
|
30
|
+
import { QUEUE_PREFIX } from './message-formatting.js'
|
|
30
31
|
import { initializeOpencodeForDirectory } from './opencode.js'
|
|
31
32
|
import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js'
|
|
32
33
|
import { notifyError } from './sentry.js'
|
|
@@ -99,7 +100,7 @@ async function executeThreadScheduledTask({
|
|
|
99
100
|
const embed = [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }]
|
|
100
101
|
// Newline between prefix and prompt so leading /command detection can
|
|
101
102
|
// find the command on its own line.
|
|
102
|
-
const prefixedPrompt =
|
|
103
|
+
const prefixedPrompt = `${QUEUE_PREFIX}**kimaki-cli:**\n${prompt}`
|
|
103
104
|
|
|
104
105
|
// Re-join the user before posting, so the message they get notified about is
|
|
105
106
|
// in a thread they are already a member of. Works on archived threads too;
|
|
@@ -16,7 +16,10 @@ import { closeDb } from './db.js'
|
|
|
16
16
|
import { runTaskCommand } from './task-runner.js'
|
|
17
17
|
import {
|
|
18
18
|
appendTaskCommandOutput,
|
|
19
|
+
applyScheduledTaskUserEdit,
|
|
19
20
|
parseScheduledTaskPayload,
|
|
21
|
+
formatSessionSleepToolOutput,
|
|
22
|
+
formatSessionSleepWakePrompt,
|
|
20
23
|
parseSendAtValue,
|
|
21
24
|
parseSleepWakeAt,
|
|
22
25
|
} from './task-schedule.js'
|
|
@@ -176,6 +179,42 @@ describe('parseSleepWakeAt', () => {
|
|
|
176
179
|
})
|
|
177
180
|
})
|
|
178
181
|
|
|
182
|
+
describe('formatSessionSleepToolOutput', () => {
|
|
183
|
+
const wakeAt = new Date('2026-09-07T13:21:00Z')
|
|
184
|
+
|
|
185
|
+
test('starts with Sleeping until and tells the model to wait', () => {
|
|
186
|
+
const output = formatSessionSleepToolOutput({
|
|
187
|
+
wakeAt,
|
|
188
|
+
reason: 'Wait 1 day, then check mail',
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
expect(output).toBe(
|
|
192
|
+
'Sleeping until 2026-09-07 13:21 UTC. Reason: Wait 1 day, then check mail. This tool result is not a wake. Do not continue the waited work. Do not call more tools. Reply with one short line that you are waiting until that time, then stop. The real wake is a later Discord message that starts with "Woke after sleeping until". Only then continue the wait reason. A new user message in this thread cancels the sleep. If you still need that later wake after answering, call kimaki_sleep again with until set to the same UTC time.',
|
|
193
|
+
)
|
|
194
|
+
expect(output.startsWith('Sleeping until ')).toBe(true)
|
|
195
|
+
expect(output).toContain('This tool result is not a wake')
|
|
196
|
+
expect(output).not.toContain('You will be woken')
|
|
197
|
+
expect(output).not.toContain('Stop now')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
test('omits reason when none is passed', () => {
|
|
201
|
+
const output = formatSessionSleepToolOutput({ wakeAt })
|
|
202
|
+
expect(output.startsWith('Sleeping until 2026-09-07 13:21 UTC.')).toBe(true)
|
|
203
|
+
expect(output).not.toContain('Reason:')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('wake prompt stays a separate later message', () => {
|
|
207
|
+
expect(
|
|
208
|
+
formatSessionSleepWakePrompt({
|
|
209
|
+
wakeAt,
|
|
210
|
+
reason: 'Wait 1 day, then check mail',
|
|
211
|
+
}),
|
|
212
|
+
).toBe(
|
|
213
|
+
'⬦ Woke after sleeping until 2026-09-07 13:21 UTC\nReason: Wait 1 day, then check mail\nContinue the work you were waiting for.',
|
|
214
|
+
)
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
|
|
179
218
|
describe('scheduled task execution options', () => {
|
|
180
219
|
const tempDirectories: string[] = []
|
|
181
220
|
|
|
@@ -210,6 +249,61 @@ describe('scheduled task execution options', () => {
|
|
|
210
249
|
`)
|
|
211
250
|
})
|
|
212
251
|
|
|
252
|
+
test('clears the stored user when task edit passes an empty --user', () => {
|
|
253
|
+
const payload = parseScheduledTaskPayload(JSON.stringify({
|
|
254
|
+
kind: 'thread',
|
|
255
|
+
threadId: 'thread-1',
|
|
256
|
+
prompt: 'Handle support requests',
|
|
257
|
+
userId: '535922349652836367',
|
|
258
|
+
username: 'Tommy',
|
|
259
|
+
}))
|
|
260
|
+
expect(payload).not.toBeInstanceOf(Error)
|
|
261
|
+
if (payload instanceof Error) throw payload
|
|
262
|
+
|
|
263
|
+
expect(applyScheduledTaskUserEdit({
|
|
264
|
+
payload,
|
|
265
|
+
userOption: '',
|
|
266
|
+
})).toMatchInlineSnapshot(`
|
|
267
|
+
{
|
|
268
|
+
"agent": null,
|
|
269
|
+
"allowConcurrency": false,
|
|
270
|
+
"injectionGuardPatterns": null,
|
|
271
|
+
"kind": "thread",
|
|
272
|
+
"model": null,
|
|
273
|
+
"parentSessionId": null,
|
|
274
|
+
"permissions": null,
|
|
275
|
+
"preRunCommand": null,
|
|
276
|
+
"prompt": "Handle support requests",
|
|
277
|
+
"threadId": "thread-1",
|
|
278
|
+
"userId": null,
|
|
279
|
+
"username": null,
|
|
280
|
+
}
|
|
281
|
+
`)
|
|
282
|
+
expect(applyScheduledTaskUserEdit({
|
|
283
|
+
payload,
|
|
284
|
+
userOption: ' ',
|
|
285
|
+
}).userId).toBeNull()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('sets the stored user when task edit passes a resolved --user', () => {
|
|
289
|
+
const payload = parseScheduledTaskPayload(JSON.stringify({
|
|
290
|
+
kind: 'channel',
|
|
291
|
+
channelId: 'chan-1',
|
|
292
|
+
prompt: 'Daily digest',
|
|
293
|
+
}))
|
|
294
|
+
expect(payload).not.toBeInstanceOf(Error)
|
|
295
|
+
if (payload instanceof Error) throw payload
|
|
296
|
+
|
|
297
|
+
expect(applyScheduledTaskUserEdit({
|
|
298
|
+
payload,
|
|
299
|
+
userOption: '535922349652836367',
|
|
300
|
+
resolvedUser: { id: '535922349652836367', username: 'Tommy' },
|
|
301
|
+
})).toMatchObject({
|
|
302
|
+
userId: '535922349652836367',
|
|
303
|
+
username: 'Tommy',
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
|
|
213
307
|
test('adds command stdout to the prompt', () => {
|
|
214
308
|
expect(appendTaskCommandOutput({
|
|
215
309
|
prompt: 'Handle support requests',
|