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
|
@@ -11,6 +11,20 @@ afterEach(async () => {
|
|
|
11
11
|
}));
|
|
12
12
|
});
|
|
13
13
|
describe('system-message', () => {
|
|
14
|
+
test('requires kimaki upload for Discord images, not markdown', () => {
|
|
15
|
+
const message = getOpencodeSystemMessage({
|
|
16
|
+
sessionId: 'ses_123',
|
|
17
|
+
});
|
|
18
|
+
expect(message).toContain('NEVER show images with markdown');
|
|
19
|
+
expect(message).toContain('Discord does not render local markdown images');
|
|
20
|
+
expect(message).toContain('ALWAYS upload them with `kimaki upload-to-discord`');
|
|
21
|
+
});
|
|
22
|
+
test('requires reading the report-bugs guide before filing kimaki issues', () => {
|
|
23
|
+
const message = getOpencodeSystemMessage({
|
|
24
|
+
sessionId: 'ses_123',
|
|
25
|
+
});
|
|
26
|
+
expect(message).toContain('Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue');
|
|
27
|
+
});
|
|
14
28
|
test('includes callout guidance for important content', () => {
|
|
15
29
|
const message = getOpencodeSystemMessage({
|
|
16
30
|
sessionId: 'ses_123',
|
|
@@ -24,6 +38,24 @@ describe('system-message', () => {
|
|
|
24
38
|
expect(message).toContain('- failed commands');
|
|
25
39
|
expect(message).toContain('<callout accent="#f59e0b">');
|
|
26
40
|
});
|
|
41
|
+
test('tells the model the sleep tool result is not a wake', () => {
|
|
42
|
+
const message = getOpencodeSystemMessage({
|
|
43
|
+
sessionId: 'ses_123',
|
|
44
|
+
});
|
|
45
|
+
expect(message).toContain('The tool result is not a wake');
|
|
46
|
+
expect(message).toContain('Woke after sleeping until');
|
|
47
|
+
expect(message).toContain('If you still need to wake later after answering, call `kimaki_sleep` again');
|
|
48
|
+
expect(message).not.toContain('After wake, continue the wait reason');
|
|
49
|
+
});
|
|
50
|
+
test('tells the model to stay quiet between tool calls', () => {
|
|
51
|
+
const message = getOpencodeSystemMessage({
|
|
52
|
+
sessionId: 'ses_123',
|
|
53
|
+
});
|
|
54
|
+
expect(message).toContain('## Discord output');
|
|
55
|
+
expect(message).toContain('Be concise');
|
|
56
|
+
expect(message).toContain('Do not narrate between tool calls');
|
|
57
|
+
expect(message).toContain('Do not output text until you are ready to give the user the final answer for this turn');
|
|
58
|
+
});
|
|
27
59
|
test('requires interactive tools after all text, using exact tool names', () => {
|
|
28
60
|
const message = getOpencodeSystemMessage({
|
|
29
61
|
sessionId: 'ses_123',
|
|
@@ -33,6 +65,7 @@ describe('system-message', () => {
|
|
|
33
65
|
expect(message).toContain('NEVER call `question` before your text');
|
|
34
66
|
expect(message).toContain('`kimaki_action_buttons`');
|
|
35
67
|
expect(message).toContain('`kimaki_file_upload`');
|
|
68
|
+
expect(message).toContain('`kimaki_sleep`');
|
|
36
69
|
});
|
|
37
70
|
test('persists and reads session system prompt for command path', async () => {
|
|
38
71
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-system-'));
|
|
@@ -67,6 +100,26 @@ describe('system-message', () => {
|
|
|
67
100
|
await fs.promises.mkdir(filePath);
|
|
68
101
|
await expect(readSessionSystemPrompt({ sessionId, dataDir })).rejects.toMatchObject({ code: 'EISDIR' });
|
|
69
102
|
});
|
|
103
|
+
test('includes all-projects session search example', () => {
|
|
104
|
+
const message = getOpencodeSystemMessage({
|
|
105
|
+
sessionId: 'ses_123',
|
|
106
|
+
channelId: 'chan_123',
|
|
107
|
+
});
|
|
108
|
+
expect(message).toContain('kimaki session search "auth timeout" --all');
|
|
109
|
+
expect(message).toContain('kimaki session search "auth timeout" --days 0');
|
|
110
|
+
expect(message).toContain('Defaults to this project and the last 14 days. Use `--days 0` for all time. Use `--all` to search every locally registered project');
|
|
111
|
+
});
|
|
112
|
+
test('includes session title update guidance when scope or goal changed', () => {
|
|
113
|
+
const message = getOpencodeSystemMessage({
|
|
114
|
+
sessionId: 'ses_123',
|
|
115
|
+
threadId: 'thread_123',
|
|
116
|
+
});
|
|
117
|
+
expect(message).toContain('## updating the session title');
|
|
118
|
+
expect(message).toContain("kimaki session title 'Short title' --session ses_123");
|
|
119
|
+
expect(message).toContain('When the session scope or goal changed from the first message, update the title:');
|
|
120
|
+
expect(message).toContain('thread-name="..."');
|
|
121
|
+
expect(message).toContain('Do not retitle every turn');
|
|
122
|
+
});
|
|
70
123
|
test('includes parent session context when parentSessionId is set', () => {
|
|
71
124
|
const message = getOpencodeSystemMessage({
|
|
72
125
|
sessionId: 'ses_child',
|
|
@@ -78,6 +131,64 @@ describe('system-message', () => {
|
|
|
78
131
|
expect(message).toContain('Do NOT message the parent session unless the user explicitly asks you to.');
|
|
79
132
|
expect(message).toContain('--parent-session ses_child');
|
|
80
133
|
});
|
|
134
|
+
test('scheduled cron task section shows task id, cron and no-sleep rule', () => {
|
|
135
|
+
const message = getOpencodeSystemMessage({
|
|
136
|
+
sessionId: 'ses_task',
|
|
137
|
+
channelId: 'chan_123',
|
|
138
|
+
threadId: 'thread_123',
|
|
139
|
+
scheduledTask: {
|
|
140
|
+
taskId: 23,
|
|
141
|
+
scheduleKind: 'cron',
|
|
142
|
+
cronExpr: '0 6,14 * * *',
|
|
143
|
+
timezone: 'Europe/Rome',
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
expect(message).toContain('## scheduled task session');
|
|
147
|
+
expect(message).toContain('kimaki scheduled task #23');
|
|
148
|
+
expect(message).toContain('Schedule: cron `0 6,14 * * *` in Europe/Rome.');
|
|
149
|
+
expect(message).toContain('Do NOT use `kimaki_sleep` to wait for the next run');
|
|
150
|
+
expect(message).toContain('starts a fresh session automatically');
|
|
151
|
+
const section = message.slice(message.indexOf('## scheduled task session'), message.indexOf('## archiving the current thread'));
|
|
152
|
+
expect(section).not.toContain('archive');
|
|
153
|
+
});
|
|
154
|
+
test('cron task without timezone says UTC', () => {
|
|
155
|
+
const message = getOpencodeSystemMessage({
|
|
156
|
+
sessionId: 'ses_task_utc',
|
|
157
|
+
scheduledTask: { taskId: 7, scheduleKind: 'cron', cronExpr: '0 9 * * 1' },
|
|
158
|
+
});
|
|
159
|
+
expect(message).toContain('Schedule: cron `0 9 * * 1` in UTC.');
|
|
160
|
+
});
|
|
161
|
+
test('one-shot at task says it does not repeat and skips sleep', () => {
|
|
162
|
+
const message = getOpencodeSystemMessage({
|
|
163
|
+
sessionId: 'ses_oneshot',
|
|
164
|
+
scheduledTask: { scheduleKind: 'at' },
|
|
165
|
+
});
|
|
166
|
+
expect(message).toContain('## scheduled task session');
|
|
167
|
+
expect(message).toContain('a one-time kimaki scheduled task');
|
|
168
|
+
expect(message).toContain('This task runs once and does not repeat.');
|
|
169
|
+
expect(message).toContain('Do NOT use `kimaki_sleep` to wait for the next run');
|
|
170
|
+
expect(message).not.toContain('Schedule: cron');
|
|
171
|
+
});
|
|
172
|
+
test('no scheduled task section by default (normal sessions)', () => {
|
|
173
|
+
const message = getOpencodeSystemMessage({
|
|
174
|
+
sessionId: 'ses_normal',
|
|
175
|
+
channelId: 'chan_123',
|
|
176
|
+
threadId: 'thread_123',
|
|
177
|
+
});
|
|
178
|
+
expect(message).not.toContain('## scheduled task session');
|
|
179
|
+
});
|
|
180
|
+
test('lets agents omit --user for quiet scheduled work', () => {
|
|
181
|
+
const message = getOpencodeSystemMessage({
|
|
182
|
+
sessionId: 'ses_123',
|
|
183
|
+
channelId: 'chan_123',
|
|
184
|
+
threadId: 'thread_123',
|
|
185
|
+
});
|
|
186
|
+
expect(message).not.toContain('ALWAYS pass `--user` when scheduling a task.');
|
|
187
|
+
expect(message).not.toContain('(always pass this)');
|
|
188
|
+
expect(message).toContain("kimaki task edit <id> --user ''");
|
|
189
|
+
expect(message).toContain("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");
|
|
190
|
+
expect(message).not.toContain("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>'");
|
|
191
|
+
});
|
|
81
192
|
test('omits parent session system block by default (btw/task/fork cache)', () => {
|
|
82
193
|
// /btw, task subagents, and normal sessions must not get a parent block in
|
|
83
194
|
// the system message. That block is opt-in via --parent-session only.
|
|
@@ -107,10 +218,17 @@ describe('system-message', () => {
|
|
|
107
218
|
],
|
|
108
219
|
}).replace(/`[^`]*\/kimaki\.log`/, '`<data-dir>/kimaki.log`');
|
|
109
220
|
expect(message).toContain('When pulling submodules and they jump to a new commit, commit that submodule pointer update right away before doing other work.');
|
|
221
|
+
expect(message).toContain('while kimaki session list --active --exclude ses_123; do sleep 5; done');
|
|
110
222
|
expect(message).toMatchInlineSnapshot(`
|
|
111
223
|
"
|
|
112
224
|
The user is reading your messages from inside Discord, via kimaki.dev
|
|
113
225
|
|
|
226
|
+
## Discord output
|
|
227
|
+
|
|
228
|
+
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.
|
|
229
|
+
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.
|
|
230
|
+
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.
|
|
231
|
+
|
|
114
232
|
## bash tool
|
|
115
233
|
|
|
116
234
|
When calling the bash tool, always include these extra fields alongside \`command\`:
|
|
@@ -127,7 +245,7 @@ describe('system-message', () => {
|
|
|
127
245
|
}
|
|
128
246
|
\`\`\`
|
|
129
247
|
|
|
130
|
-
\`description\` is shown
|
|
248
|
+
\`description\` is shown in Discord when the bash command is longer than 50 characters.
|
|
131
249
|
\`hasSideEffect\` distinguishes essential bash calls from read-only ones in low-verbosity mode.
|
|
132
250
|
|
|
133
251
|
Your current OpenCode session ID is: ses_123
|
|
@@ -135,7 +253,7 @@ describe('system-message', () => {
|
|
|
135
253
|
Your current Discord thread ID is: thread_123
|
|
136
254
|
Your current Discord guild ID is: guild_123
|
|
137
255
|
|
|
138
|
-
Per-turn Discord metadata like the current user
|
|
256
|
+
Per-turn Discord metadata like the current user, current agent, and Discord thread title is delivered in synthetic user message parts.
|
|
139
257
|
|
|
140
258
|
## permissions
|
|
141
259
|
|
|
@@ -158,6 +276,7 @@ describe('system-message', () => {
|
|
|
158
276
|
|
|
159
277
|
## debugging kimaki issues
|
|
160
278
|
|
|
279
|
+
ALWAYS read https://kimaki.dev/docs/guides/report-bugs first before submitting any issue to Kimaki. That page is the source of truth for exporting session jsonl, sharing evidence in a gist, and filing bugs. Never open a pull request on remorses/kimaki unless remorses asked for one in a comment on the issue.
|
|
161
280
|
If there are internal kimaki issues (sessions not responding, bot errors, unexpected behavior), read the log file at \`<data-dir>/kimaki.log\`. This file contains detailed logs of all bot activity including session creation, event handling, errors, and API calls. The log file is reset every time the bot restarts, so it only contains logs from the current run.
|
|
162
281
|
|
|
163
282
|
## uploading files to discord
|
|
@@ -166,6 +285,8 @@ describe('system-message', () => {
|
|
|
166
285
|
|
|
167
286
|
kimaki upload-to-discord --session ses_123 <file1> [file2] ...
|
|
168
287
|
|
|
288
|
+
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.
|
|
289
|
+
|
|
169
290
|
## generating audio from text
|
|
170
291
|
|
|
171
292
|
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.
|
|
@@ -187,9 +308,18 @@ describe('system-message', () => {
|
|
|
187
308
|
To ask the user to upload files from their device, use \`kimaki_file_upload\`. This shows a native file picker dialog in Discord. The files are downloaded to the project's \`uploads/\` directory and the tool returns the local file paths.
|
|
188
309
|
You MUST call \`kimaki_file_upload\` LAST, after ALL text.
|
|
189
310
|
|
|
311
|
+
## sleeping the session
|
|
312
|
+
|
|
313
|
+
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.
|
|
314
|
+
Pass either \`duration\` (\`30s\`, \`2h\`, \`1d\`) or \`until\` (UTC ISO ending with \`Z\`, example \`2026-08-20T09:00:00Z\`).
|
|
315
|
+
You MUST call \`kimaki_sleep\` LAST, after ALL text. Do not call more tools after it.
|
|
316
|
+
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.
|
|
317
|
+
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.
|
|
318
|
+
Wake is a later Discord message that starts with \`Woke after sleeping until\`. Only then continue the wait reason.
|
|
319
|
+
|
|
190
320
|
## archiving the current thread
|
|
191
321
|
|
|
192
|
-
To archive the current Discord thread (hide it from sidebar)
|
|
322
|
+
To archive the current Discord thread (hide it from sidebar) without stopping the session, run:
|
|
193
323
|
|
|
194
324
|
kimaki session archive thread_123 (or --session ses_123)
|
|
195
325
|
|
|
@@ -204,6 +334,17 @@ describe('system-message', () => {
|
|
|
204
334
|
This stops the AI from processing but keeps the thread visible in Discord.
|
|
205
335
|
Different from \`kimaki session archive\` which hides the thread.
|
|
206
336
|
|
|
337
|
+
## updating the session title
|
|
338
|
+
|
|
339
|
+
When the session scope or goal changed from the first message, update the title:
|
|
340
|
+
|
|
341
|
+
kimaki session title 'Short title' --session ses_123
|
|
342
|
+
|
|
343
|
+
The current Discord thread title is in the per-turn \`<discord-user thread-name="..." />\` metadata.
|
|
344
|
+
This updates the OpenCode title. Discord follows automatically.
|
|
345
|
+
Do not retitle every turn. Discord rate-limits thread renames.
|
|
346
|
+
Keep titles short. Do not add emoji. Do not copy ⬦, btw:, or Fork: prefixes.
|
|
347
|
+
|
|
207
348
|
## discord user mentions
|
|
208
349
|
|
|
209
350
|
Prefer Discord user IDs for mentions. Discord bots cannot ping by @name; use \`<@userId>\` in message text or pass the ID to \`--user\`.
|
|
@@ -304,13 +445,13 @@ describe('system-message', () => {
|
|
|
304
445
|
Use \`--send-at\` to schedule a one-time or recurring task:
|
|
305
446
|
|
|
306
447
|
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>'
|
|
307
|
-
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
|
|
448
|
+
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
|
|
308
449
|
|
|
309
450
|
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.
|
|
310
451
|
|
|
311
452
|
Scheduled tasks do not overlap by default. Add \`--allow-concurrency\` only when concurrent sessions from the same task are safe.
|
|
312
453
|
|
|
313
|
-
|
|
454
|
+
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.
|
|
314
455
|
|
|
315
456
|
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).
|
|
316
457
|
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.
|
|
@@ -322,14 +463,14 @@ describe('system-message', () => {
|
|
|
322
463
|
- \`--pre-run\` to start only when a project command exits with code 0
|
|
323
464
|
- \`--allow-concurrency\` to permit overlapping runs from the same task
|
|
324
465
|
- \`--parent-session\` to pass this session as parent of the scheduled child
|
|
325
|
-
- \`--user\` to add a specific user to the scheduled thread
|
|
466
|
+
- \`--user\` to add a specific user to the scheduled thread. Omit this for quiet autonomous work
|
|
326
467
|
|
|
327
468
|
\`--wait\` is incompatible with \`--send-at\` because scheduled tasks run in the future.
|
|
328
469
|
|
|
329
470
|
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:
|
|
330
471
|
|
|
331
472
|
\`\`\`bash
|
|
332
|
-
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
|
|
473
|
+
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
|
|
333
474
|
\`\`\`
|
|
334
475
|
|
|
335
476
|
The task file should contain all the detail: goal, constraints, expected output, completion criteria. Use this frontmatter format:
|
|
@@ -348,7 +489,7 @@ describe('system-message', () => {
|
|
|
348
489
|
Notification strategy:
|
|
349
490
|
- 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.
|
|
350
491
|
- If a task needs user attention, add "mention the user via Discord user ID when task requires user review" in the task md file.
|
|
351
|
-
- With \`--user\`, the user is added to the thread and receives thread-level notifications.
|
|
492
|
+
- 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.
|
|
352
493
|
- If a scheduled task completes with no actionable result, archive the session: \`kimaki session archive thread_123 (or --session ses_123)\`
|
|
353
494
|
|
|
354
495
|
Manage scheduled tasks with:
|
|
@@ -357,7 +498,7 @@ describe('system-message', () => {
|
|
|
357
498
|
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>"]
|
|
358
499
|
kimaki task delete <id>
|
|
359
500
|
|
|
360
|
-
\`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.
|
|
501
|
+
\`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.
|
|
361
502
|
|
|
362
503
|
\`kimaki session list\` also shows if a session was started by a scheduled \`delay\` or \`cron\` task, including task ID when available.
|
|
363
504
|
|
|
@@ -435,15 +576,22 @@ describe('system-message', () => {
|
|
|
435
576
|
kimaki session list
|
|
436
577
|
kimaki session list --json # machine-readable output
|
|
437
578
|
kimaki session list --project /path/to/project # specific project
|
|
579
|
+
|
|
580
|
+
# List only in-progress sessions. Exit status is 1 when none remain.
|
|
581
|
+
kimaki session list --active
|
|
438
582
|
\`\`\`
|
|
439
583
|
|
|
440
|
-
|
|
584
|
+
Titles prefixed with \`btw:\` are side sessions that answer a related user question in parallel. They are not duplicate sessions of the main task.
|
|
585
|
+
|
|
586
|
+
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:
|
|
441
587
|
|
|
442
588
|
\`\`\`bash
|
|
443
589
|
kimaki session search "auth timeout"
|
|
590
|
+
kimaki session search "auth timeout" --days 0
|
|
444
591
|
kimaki session search "/error\\s+42/i"
|
|
445
592
|
kimaki session search "rate limit" --project /path/to/project
|
|
446
593
|
kimaki session search "/panic|crash/i" --channel <channel_id>
|
|
594
|
+
kimaki session search "auth timeout" --all
|
|
447
595
|
\`\`\`
|
|
448
596
|
|
|
449
597
|
To read a session's full conversation as markdown, pipe to a file and grep it to avoid wasting context.
|
|
@@ -455,6 +603,29 @@ describe('system-message', () => {
|
|
|
455
603
|
|
|
456
604
|
Then use grep/read tools on the file to find what you need.
|
|
457
605
|
|
|
606
|
+
### who edited a file
|
|
607
|
+
|
|
608
|
+
To find the session that last edited a file, run:
|
|
609
|
+
|
|
610
|
+
\`\`\`bash
|
|
611
|
+
kimaki session editors src/foo.ts
|
|
612
|
+
kimaki session editors src/foo.ts --json
|
|
613
|
+
\`\`\`
|
|
614
|
+
|
|
615
|
+
Output is newest first. Each row has:
|
|
616
|
+
|
|
617
|
+
- **session ID** (\`ses_xxx\`)
|
|
618
|
+
- **title** (Discord thread name, so you can tell what that session was doing)
|
|
619
|
+
- **time ago** (when that session last edited the file)
|
|
620
|
+
|
|
621
|
+
Use this before a commit when this session did not edit the file. Put the original session ID as the last line of the commit message:
|
|
622
|
+
|
|
623
|
+
\`\`\`
|
|
624
|
+
Session: ses_xxx
|
|
625
|
+
\`\`\`
|
|
626
|
+
|
|
627
|
+
If this session edited the file, use this session ID instead. If several files come from different sessions, split the commit by session. Do not attribute another session's edits to this one.
|
|
628
|
+
|
|
458
629
|
## cross-project commands
|
|
459
630
|
|
|
460
631
|
When the user references another project by name, run \`kimaki project list\` to find its directory path and channel ID. Then read files, search code, or run commands directly in that directory. If the project is not listed, use \`kimaki project add /path/to/repo\` to register it and create a Discord channel for it. Do not add subfolders of an existing project — only add root project directories.
|
|
@@ -521,7 +692,7 @@ describe('system-message', () => {
|
|
|
521
692
|
|
|
522
693
|
When the user asks you to wait for an existing session, run \`kimaki session wait <session_id>\` yourself via Bash, then continue from the printed session markdown. Do not tell the user to run the command.
|
|
523
694
|
|
|
524
|
-
IMPORTANT: if you run \`kimaki send --wait
|
|
695
|
+
IMPORTANT: if you run \`kimaki send --wait\`, \`kimaki session wait <session_id>\`, or the active-session wait loop via the Bash tool, you must set the Bash tool \`timeout\` to **20 minutes or more** (example: \`timeout: 1_500_000\`). Otherwise the tool will terminate early (default is 2 minutes) and you won't see long sessions.
|
|
525
696
|
|
|
526
697
|
If your Bash tool timeout triggers anyway, fall back to reading the session output from disk:
|
|
527
698
|
|
|
@@ -536,9 +707,14 @@ describe('system-message', () => {
|
|
|
536
707
|
|
|
537
708
|
# Wait for a session that was already started elsewhere
|
|
538
709
|
kimaki session wait <session_id>
|
|
710
|
+
|
|
711
|
+
# Wait until every other in-progress session in this project finishes
|
|
712
|
+
while kimaki session list --active --exclude ses_123; do sleep 5; done
|
|
539
713
|
\`\`\`
|
|
540
714
|
|
|
541
|
-
|
|
715
|
+
\`session list --active\` exits with status 0 while it finds active sessions and status 1 when none remain. Exclude the current session in a wait loop so the loop does not wait for itself.
|
|
716
|
+
|
|
717
|
+
\`session wait\` exits with the session markdown on stdout once the model finishes responding.
|
|
542
718
|
|
|
543
719
|
Use \`--wait\` when you need to:
|
|
544
720
|
- **Fix a bug in another project** before continuing here (e.g. fix a dependency, then resume)
|
|
@@ -625,7 +801,7 @@ describe('system-message', () => {
|
|
|
625
801
|
|
|
626
802
|
## running dev servers with tunnel access
|
|
627
803
|
|
|
628
|
-
|
|
804
|
+
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\`.
|
|
629
805
|
|
|
630
806
|
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.
|
|
631
807
|
|
|
@@ -803,7 +979,7 @@ describe('system-message', () => {
|
|
|
803
979
|
You MUST call \`question\` LAST, after ALL text parts.
|
|
804
980
|
NEVER call \`question\` before your text. Discord will hide the message.
|
|
805
981
|
|
|
806
|
-
The same rule applies to \`kimaki_action_buttons\` and \`
|
|
982
|
+
The same rule applies to \`kimaki_action_buttons\`, \`kimaki_file_upload\`, and \`kimaki_sleep\`.
|
|
807
983
|
You MUST call them LAST, after ALL text.
|
|
808
984
|
|
|
809
985
|
ALWAYS use \`question\` when you ask the user a question. Do not write a numbered list in plain text.
|
|
@@ -831,6 +1007,7 @@ describe('system-message', () => {
|
|
|
831
1007
|
userId: 'user_123',
|
|
832
1008
|
sourceMessageId: 'msg_123',
|
|
833
1009
|
sourceThreadId: 'thread_123',
|
|
1010
|
+
threadName: 'Fix auth bug',
|
|
834
1011
|
repliedMessage: {
|
|
835
1012
|
authorUsername: 'alice',
|
|
836
1013
|
text: 'Original replied message',
|
|
@@ -843,7 +1020,7 @@ describe('system-message', () => {
|
|
|
843
1020
|
mainRepoDirectory: '/repo',
|
|
844
1021
|
},
|
|
845
1022
|
})).toMatchInlineSnapshot(`
|
|
846
|
-
"<discord-user name="Tommy" user-id="user_123" message-id="msg_123" thread-id="thread_123" />
|
|
1023
|
+
"<discord-user name="Tommy" user-id="user_123" message-id="msg_123" thread-id="thread_123" thread-name="Fix auth bug" />
|
|
847
1024
|
|
|
848
1025
|
This message was a reply to message
|
|
849
1026
|
|
package/dist/task-runner.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
// Scheduled task runner for
|
|
2
|
-
import { Routes } from 'discord.js';
|
|
1
|
+
// Scheduled task runner for due `send --send-at` jobs and session sleep wakes.
|
|
2
|
+
import { DiscordAPIError, Routes } from 'discord.js';
|
|
3
3
|
import { createDiscordRest } from './discord-urls.js';
|
|
4
4
|
import { ensureThreadMember } from './discord-utils.js';
|
|
5
5
|
import YAML from 'yaml';
|
|
6
|
-
import { claimScheduledTaskRunning, createScheduledTaskRun, failScheduledTaskRun, finishScheduledTaskRun, getDuePlannedScheduledTasks, getScheduledTask, getActiveScheduledTaskRuns, markScheduledTaskCronRescheduled, markScheduledTaskCronRetry, markScheduledTaskFailed, deleteScheduledTask, recoverStaleRunningScheduledTasks, releaseScheduledTaskClaim, setScheduledTaskRunThread, } from './database.js';
|
|
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';
|
|
8
|
+
import { QUEUE_PREFIX } from './message-formatting.js';
|
|
8
9
|
import { initializeOpencodeForDirectory } from './opencode.js';
|
|
9
10
|
import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js';
|
|
10
11
|
import { notifyError } from './sentry.js';
|
|
11
|
-
import { appendTaskCommandOutput, getNextCronRun, getPromptPreview, parseScheduledTaskPayload, } from './task-schedule.js';
|
|
12
|
+
import { appendTaskCommandOutput, formatSessionSleepWakePrompt, getNextCronRun, getPromptPreview, parseScheduledTaskPayload, } from './task-schedule.js';
|
|
12
13
|
const taskLogger = createLogger(LogPrefix.TASK);
|
|
13
14
|
const MAX_SCHEDULED_PROMPT_LENGTH = 1_900;
|
|
14
15
|
const PENDING_RUN_TIMEOUT_MS = 120_000;
|
|
@@ -46,7 +47,7 @@ async function executeThreadScheduledTask({ rest, task, payload, prompt, runId,
|
|
|
46
47
|
const embed = [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }];
|
|
47
48
|
// Newline between prefix and prompt so leading /command detection can
|
|
48
49
|
// find the command on its own line.
|
|
49
|
-
const prefixedPrompt =
|
|
50
|
+
const prefixedPrompt = `${QUEUE_PREFIX}**kimaki-cli:**\n${prompt}`;
|
|
50
51
|
// Re-join the user before posting, so the message they get notified about is
|
|
51
52
|
// in a thread they are already a member of. Works on archived threads too;
|
|
52
53
|
// the post below auto-unarchives.
|
|
@@ -76,6 +77,95 @@ async function executeThreadScheduledTask({ rest, task, payload, prompt, runId,
|
|
|
76
77
|
return postResult;
|
|
77
78
|
return payload.threadId;
|
|
78
79
|
}
|
|
80
|
+
/** Wait before retrying a wake that ingress has not consumed yet. */
|
|
81
|
+
const SLEEP_WAKE_RETRY_AFTER_MS = 30_000;
|
|
82
|
+
const SLEEP_WAKE_MAX_ATTEMPTS = 5;
|
|
83
|
+
export function buildSessionSleepWakeBody({ deliveryId, wakeAt, reason, }) {
|
|
84
|
+
const marker = {
|
|
85
|
+
start: true,
|
|
86
|
+
sleepWake: true,
|
|
87
|
+
sleepId: deliveryId,
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
content: formatSessionSleepWakePrompt({ wakeAt, reason }),
|
|
91
|
+
embeds: [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }],
|
|
92
|
+
// Discord limits nonces to 25 characters. The full ID stays in the marker.
|
|
93
|
+
// A stable nonce deduplicates retries when a successful response is lost.
|
|
94
|
+
nonce: deliveryId.replaceAll('-', '').slice(0, 25),
|
|
95
|
+
enforce_nonce: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
async function postSessionSleepWake({ rest, sleep, threadId, }) {
|
|
99
|
+
// `.then(() => null)` keeps the success type concrete. Returning the raw post
|
|
100
|
+
// value would widen the union to `unknown` and erase the failure shape.
|
|
101
|
+
return await rest
|
|
102
|
+
.post(Routes.channelMessages(threadId), {
|
|
103
|
+
body: buildSessionSleepWakeBody({
|
|
104
|
+
deliveryId: sleep.delivery_id,
|
|
105
|
+
wakeAt: sleep.wake_at,
|
|
106
|
+
reason: sleep.reason,
|
|
107
|
+
}),
|
|
108
|
+
})
|
|
109
|
+
.then(() => {
|
|
110
|
+
return null;
|
|
111
|
+
})
|
|
112
|
+
.catch((error) => {
|
|
113
|
+
// Only an unknown channel/message is genuinely unrecoverable. A 403 is
|
|
114
|
+
// often temporary (permissions revoked, thread locked) so it keeps
|
|
115
|
+
// retrying until the attempt budget runs out.
|
|
116
|
+
const permanent = error instanceof DiscordAPIError &&
|
|
117
|
+
error.status === 404;
|
|
118
|
+
return {
|
|
119
|
+
error: new Error(`Failed to post sleep wake for session ${sleep.session_id}`, { cause: error }),
|
|
120
|
+
permanent,
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export async function wakeDueSessionSleeps({ rest, now = new Date(), limit = 20, }) {
|
|
125
|
+
const dueSleeps = await getDueSessionSleeps({
|
|
126
|
+
now,
|
|
127
|
+
retryAfterMs: SLEEP_WAKE_RETRY_AFTER_MS,
|
|
128
|
+
limit,
|
|
129
|
+
});
|
|
130
|
+
for (const sleep of dueSleeps) {
|
|
131
|
+
// Reserves an attempt but stays `planned`. The row only leaves `planned`
|
|
132
|
+
// when ingress consumes the wake, so dying anywhere below costs one retry
|
|
133
|
+
// delay instead of losing the wake.
|
|
134
|
+
const claimed = await claimSessionSleepAttempt({
|
|
135
|
+
sessionId: sleep.session_id,
|
|
136
|
+
deliveryId: sleep.delivery_id,
|
|
137
|
+
now,
|
|
138
|
+
retryAfterMs: SLEEP_WAKE_RETRY_AFTER_MS,
|
|
139
|
+
});
|
|
140
|
+
if (!claimed)
|
|
141
|
+
continue;
|
|
142
|
+
// Resolved now, not when the tool ran, so a session rebound by /resume
|
|
143
|
+
// wakes in the thread that currently owns it.
|
|
144
|
+
const threadId = await getThreadIdBySessionId(claimed.session_id);
|
|
145
|
+
if (!threadId) {
|
|
146
|
+
await markSessionSleepFailed({
|
|
147
|
+
sessionId: claimed.session_id,
|
|
148
|
+
deliveryId: claimed.delivery_id,
|
|
149
|
+
});
|
|
150
|
+
taskLogger.warn(`[task-runner] no thread owns session ${claimed.session_id}, dropping sleep wake`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
// A successful post changes nothing here on purpose: ingress owns the
|
|
154
|
+
// transition out of `planned` when the wake actually becomes a turn.
|
|
155
|
+
const postResult = await postSessionSleepWake({ rest, sleep: claimed, threadId });
|
|
156
|
+
if (!postResult)
|
|
157
|
+
continue;
|
|
158
|
+
const exhausted = claimed.attempts >= SLEEP_WAKE_MAX_ATTEMPTS;
|
|
159
|
+
if (postResult.permanent || exhausted) {
|
|
160
|
+
await markSessionSleepFailed({
|
|
161
|
+
sessionId: claimed.session_id,
|
|
162
|
+
deliveryId: claimed.delivery_id,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
taskLogger.error(`[task-runner] ${formatErrorWithStack(postResult.error)}`);
|
|
166
|
+
void notifyError(postResult.error, 'Session sleep wake failed');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
79
169
|
async function executeChannelScheduledTask({ rest, task, payload, prompt, runId, }) {
|
|
80
170
|
const marker = payload.notifyOnly
|
|
81
171
|
? undefined
|
|
@@ -407,6 +497,7 @@ async function runTaskRunnerTick({ rest, staleRunningMs, dueBatchSize, }) {
|
|
|
407
497
|
await previous;
|
|
408
498
|
await processDueTask({ rest, task });
|
|
409
499
|
}, Promise.resolve());
|
|
500
|
+
await wakeDueSessionSleeps({ rest });
|
|
410
501
|
}
|
|
411
502
|
export function startTaskRunner({ token, pollIntervalMs = 5_000, staleRunningMs = 120_000, dueBatchSize = 20, }) {
|
|
412
503
|
const rest = createDiscordRest(token);
|
package/dist/task-schedule.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
// Scheduled task parsing
|
|
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 { STATUS_PREFIX } from './message-formatting.js';
|
|
4
5
|
const UTC_SEND_AT_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?Z$/;
|
|
5
6
|
export function getLocalTimeZone() {
|
|
6
7
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -33,6 +34,75 @@ function parseUtcSendAtDate({ value, now, }) {
|
|
|
33
34
|
}
|
|
34
35
|
return runAt;
|
|
35
36
|
}
|
|
37
|
+
const SLEEP_DURATION_REGEX = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i;
|
|
38
|
+
const SLEEP_DURATION_MS = {
|
|
39
|
+
ms: 1,
|
|
40
|
+
s: 1000,
|
|
41
|
+
m: 60_000,
|
|
42
|
+
h: 3_600_000,
|
|
43
|
+
d: 86_400_000,
|
|
44
|
+
};
|
|
45
|
+
function parseUtcFutureDate({ value, now, field, }) {
|
|
46
|
+
if (!UTC_SEND_AT_DATE_REGEX.test(value)) {
|
|
47
|
+
return new Error(`${field} must be UTC ISO format ending with Z (example: 2026-08-20T09:00:00Z). Received: ${value}`);
|
|
48
|
+
}
|
|
49
|
+
const parsed = new Date(value);
|
|
50
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
51
|
+
return new Error(`Invalid UTC date for ${field}: ${value}`);
|
|
52
|
+
}
|
|
53
|
+
if (parsed.getTime() <= now.getTime()) {
|
|
54
|
+
return new Error(`${field} must be in the future (UTC): ${value}`);
|
|
55
|
+
}
|
|
56
|
+
return parsed;
|
|
57
|
+
}
|
|
58
|
+
export function parseSleepWakeAt({ duration, until, now, }) {
|
|
59
|
+
const trimmedDuration = duration?.trim() || '';
|
|
60
|
+
const trimmedUntil = until?.trim() || '';
|
|
61
|
+
if (trimmedDuration && trimmedUntil) {
|
|
62
|
+
return new Error('Pass either duration or until, not both');
|
|
63
|
+
}
|
|
64
|
+
if (!trimmedDuration && !trimmedUntil) {
|
|
65
|
+
return new Error('Pass duration or until');
|
|
66
|
+
}
|
|
67
|
+
if (trimmedUntil) {
|
|
68
|
+
return parseUtcFutureDate({
|
|
69
|
+
value: trimmedUntil,
|
|
70
|
+
now,
|
|
71
|
+
field: 'until',
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const match = SLEEP_DURATION_REGEX.exec(trimmedDuration);
|
|
75
|
+
if (!match) {
|
|
76
|
+
return new Error(`Invalid duration: "${trimmedDuration}". Use a number plus ms, s, m, h, or d (example: 2h).`);
|
|
77
|
+
}
|
|
78
|
+
const amount = Number(match[1]);
|
|
79
|
+
const unit = match[2].toLowerCase();
|
|
80
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
81
|
+
return new Error('duration must be greater than 0');
|
|
82
|
+
}
|
|
83
|
+
return new Date(now.getTime() + amount * SLEEP_DURATION_MS[unit]);
|
|
84
|
+
}
|
|
85
|
+
export function formatSessionSleepWakeAt(wakeAt) {
|
|
86
|
+
return `${wakeAt.toISOString().slice(0, 16).replace('T', ' ')} UTC`;
|
|
87
|
+
}
|
|
88
|
+
export function formatSessionSleepWakePrompt({ wakeAt, reason, }) {
|
|
89
|
+
const until = formatSessionSleepWakeAt(wakeAt);
|
|
90
|
+
const reasonLine = reason?.trim() ? `\nReason: ${reason.trim()}` : '';
|
|
91
|
+
return `${STATUS_PREFIX}Woke after sleeping until ${until}${reasonLine}\nContinue the work you were waiting for.`;
|
|
92
|
+
}
|
|
93
|
+
// Tool result after persist. Keep "Sleeping until" first so e2e matchers still hit.
|
|
94
|
+
// Do not say "woken" or "Woke after": models treat that as the wake turn.
|
|
95
|
+
export function formatSessionSleepToolOutput({ wakeAt, reason, }) {
|
|
96
|
+
const until = formatSessionSleepWakeAt(wakeAt);
|
|
97
|
+
const reasonText = reason?.trim() ? ` Reason: ${reason.trim()}.` : '';
|
|
98
|
+
return [
|
|
99
|
+
`Sleeping until ${until}.${reasonText}`,
|
|
100
|
+
'This tool result is not a wake. Do not continue the waited work. Do not call more tools.',
|
|
101
|
+
'Reply with one short line that you are waiting until that time, then stop.',
|
|
102
|
+
'The real wake is a later Discord message that starts with "Woke after sleeping until". Only then continue the wait reason.',
|
|
103
|
+
'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.',
|
|
104
|
+
].join(' ');
|
|
105
|
+
}
|
|
36
106
|
export function parseSendAtValue({ value, now, timezone, }) {
|
|
37
107
|
const trimmed = value.trim();
|
|
38
108
|
if (!trimmed) {
|
|
@@ -106,6 +176,22 @@ export function getNextCronRun({ cronExpr, timezone, from, }) {
|
|
|
106
176
|
export function serializeScheduledTaskPayload(payload) {
|
|
107
177
|
return JSON.stringify(payload);
|
|
108
178
|
}
|
|
179
|
+
export function applyScheduledTaskUserEdit({ payload, userOption, resolvedUser, }) {
|
|
180
|
+
if (!userOption.trim()) {
|
|
181
|
+
return {
|
|
182
|
+
...payload,
|
|
183
|
+
userId: null,
|
|
184
|
+
username: null,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (!resolvedUser)
|
|
188
|
+
return payload;
|
|
189
|
+
return {
|
|
190
|
+
...payload,
|
|
191
|
+
userId: resolvedUser.id,
|
|
192
|
+
username: resolvedUser.username || null,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
109
195
|
export function appendTaskCommandOutput({ prompt, stdout, }) {
|
|
110
196
|
const output = stdout.trim();
|
|
111
197
|
if (!output)
|