kimaki 1.3.0 → 1.5.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/assets/fonts/Geist-Bold.ttf +0 -0
- package/assets/fonts/Geist-Regular.ttf +0 -0
- package/assets/fonts/GeistMono-Regular.ttf +0 -0
- package/assets/fonts/OFL.txt +92 -0
- package/dist/agent-ui.e2e.test.js +27 -13
- package/dist/agent-ui.e2e.test.js.map +1 -1
- package/dist/agent-ui.js +2 -1
- package/dist/agent-ui.js.map +1 -1
- package/dist/analytics.e2e.test.js +1 -1
- package/dist/analytics.e2e.test.js.map +1 -1
- package/dist/autostart.d.ts +4 -0
- package/dist/autostart.js +55 -10
- package/dist/autostart.js.map +1 -1
- package/dist/bot.d.ts +13 -6
- package/dist/bot.js +17 -15
- package/dist/bot.js.map +1 -1
- package/dist/btw.e2e.test.js +3 -3
- package/dist/builtin-skills.d.ts +3 -0
- package/dist/builtin-skills.js +29 -0
- package/dist/builtin-skills.js.map +1 -1
- package/dist/builtin-skills.test.js +72 -1
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/catalog-refresh.e2e.test.js +2 -2
- package/dist/cli/bot.d.ts +1 -1
- package/dist/cli/bot.js +33 -18
- package/dist/cli/bot.js.map +1 -1
- package/dist/cli/discord.js +32 -6
- package/dist/cli/discord.js.map +1 -1
- package/dist/cli/schedule.js +23 -9
- package/dist/cli/schedule.js.map +1 -1
- package/dist/cli/session.js +37 -12
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/shared.d.ts +5 -3
- package/dist/cli/shared.js +45 -10
- package/dist/cli/shared.js.map +1 -1
- package/dist/cli-discord.e2e.test.js +13 -8
- package/dist/cli-discord.e2e.test.js.map +1 -1
- package/dist/cli-lock.e2e.test.js +87 -20
- package/dist/cli-lock.e2e.test.js.map +1 -1
- package/dist/cli-read.e2e.test.js +18 -9
- package/dist/cli-read.e2e.test.js.map +1 -1
- package/dist/cli-sessions.e2e.test.js +4 -3
- package/dist/cli-sessions.e2e.test.js.map +1 -1
- package/dist/cli.js +2 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/preference-commands.d.ts +4 -0
- package/dist/commands/preference-commands.js +38 -4
- package/dist/commands/preference-commands.js.map +1 -1
- package/dist/commands/session-commands.js +11 -8
- package/dist/commands/session-commands.js.map +1 -1
- package/dist/commands/thread-commands.js +94 -24
- package/dist/commands/thread-commands.js.map +1 -1
- package/dist/commands/worktree-commands.d.ts +5 -6
- package/dist/commands/worktree-commands.js +8 -7
- package/dist/commands/worktree-commands.js.map +1 -1
- package/dist/commands.e2e.test.js +25 -21
- package/dist/commands.e2e.test.js.map +1 -1
- package/dist/coordinator-agents.e2e.test.d.ts +1 -0
- package/dist/coordinator-agents.e2e.test.js +113 -0
- package/dist/coordinator-agents.e2e.test.js.map +1 -0
- package/dist/coordinator-idle.e2e.test.d.ts +1 -0
- package/dist/coordinator-idle.e2e.test.js +100 -0
- package/dist/coordinator-idle.e2e.test.js.map +1 -0
- package/dist/coordinator-queue.e2e.test.d.ts +1 -0
- package/dist/coordinator-queue.e2e.test.js +244 -0
- package/dist/coordinator-queue.e2e.test.js.map +1 -0
- package/dist/coordinator-warmup.e2e.test.d.ts +1 -0
- package/dist/coordinator-warmup.e2e.test.js +131 -0
- package/dist/coordinator-warmup.e2e.test.js.map +1 -0
- package/dist/coordinator.d.ts +6 -0
- package/dist/coordinator.e2e.test.d.ts +1 -0
- package/dist/coordinator.e2e.test.js +505 -0
- package/dist/coordinator.e2e.test.js.map +1 -0
- package/dist/coordinator.js +164 -0
- package/dist/coordinator.js.map +1 -0
- package/dist/db.d.ts +78 -3
- package/dist/db.test.js +1 -0
- package/dist/db.test.js.map +1 -1
- package/dist/diagram.d.ts +11 -0
- package/dist/diagram.js +232 -0
- package/dist/diagram.js.map +1 -0
- package/dist/diagram.test.d.ts +1 -0
- package/dist/diagram.test.js +52 -0
- package/dist/diagram.test.js.map +1 -0
- package/dist/duration.js +13 -5
- package/dist/duration.js.map +1 -1
- package/dist/effects.d.ts +6 -1
- package/dist/effects.js +97 -26
- package/dist/effects.js.map +1 -1
- package/dist/event-loop.d.ts +2 -3
- package/dist/event-loop.js +74 -44
- package/dist/event-loop.js.map +1 -1
- package/dist/format-parts.d.ts +21 -4
- package/dist/format-parts.js +40 -23
- package/dist/format-parts.js.map +1 -1
- package/dist/gateway-replay.e2e.test.js +4 -4
- package/dist/harness.e2e.test.js +2 -2
- package/dist/harness.e2e.test.js.map +1 -1
- package/dist/hello-thread.e2e.test.js +56 -5
- package/dist/hello-thread.e2e.test.js.map +1 -1
- package/dist/ingress.js +47 -12
- package/dist/ingress.js.map +1 -1
- package/dist/interaction-context.d.ts +4 -4
- package/dist/interaction-context.js +9 -7
- package/dist/interaction-context.js.map +1 -1
- package/dist/interrupt.e2e.test.js +8 -5
- package/dist/interrupt.e2e.test.js.map +1 -1
- package/dist/legacy.e2e.test.js +1 -1
- package/dist/lock-routes.d.ts +83 -31
- package/dist/lock-routes.js +68 -8
- package/dist/lock-routes.js.map +1 -1
- package/dist/lock-server.d.ts +3 -2
- package/dist/lock-server.js +23 -7
- package/dist/lock-server.js.map +1 -1
- package/dist/main.d.ts +6 -3
- package/dist/main.js +14 -4
- package/dist/main.js.map +1 -1
- package/dist/mainmast-idle.e2e.test.d.ts +1 -0
- package/dist/mainmast-idle.e2e.test.js +99 -0
- package/dist/mainmast-idle.e2e.test.js.map +1 -0
- package/dist/mainmast-queue.e2e.test.d.ts +1 -0
- package/dist/mainmast-queue.e2e.test.js +244 -0
- package/dist/mainmast-queue.e2e.test.js.map +1 -0
- package/dist/mainmast-warmup.e2e.test.d.ts +1 -0
- package/dist/mainmast-warmup.e2e.test.js +130 -0
- package/dist/mainmast-warmup.e2e.test.js.map +1 -0
- package/dist/mainmast.d.ts +6 -0
- package/dist/mainmast.e2e.test.d.ts +1 -0
- package/dist/mainmast.e2e.test.js +428 -0
- package/dist/mainmast.e2e.test.js.map +1 -0
- package/dist/mainmast.js +156 -0
- package/dist/mainmast.js.map +1 -0
- package/dist/markdown/markdown.test.js +14 -4
- package/dist/markdown/markdown.test.js.map +1 -1
- package/dist/markdown/render-markdown.js +9 -6
- package/dist/markdown/render-markdown.js.map +1 -1
- package/dist/onboarding-cli.e2e.test.js +7 -6
- package/dist/onboarding-cli.e2e.test.js.map +1 -1
- package/dist/onboarding.d.ts +3 -10
- package/dist/onboarding.e2e.test.js +8 -10
- package/dist/onboarding.e2e.test.js.map +1 -1
- package/dist/onboarding.js +14 -24
- package/dist/onboarding.js.map +1 -1
- package/dist/permissions.e2e.test.js +3 -3
- package/dist/plugin/coordinator.d.ts +4 -0
- package/dist/plugin/coordinator.js +389 -0
- package/dist/plugin/coordinator.js.map +1 -0
- package/dist/plugin/index.js +93 -61
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/mainmast.d.ts +4 -0
- package/dist/plugin/mainmast.js +379 -0
- package/dist/plugin/mainmast.js.map +1 -0
- package/dist/plugin.e2e.test.js +1 -1
- package/dist/preference-commands.e2e.test.js +103 -24
- package/dist/preference-commands.e2e.test.js.map +1 -1
- package/dist/project.d.ts +1 -1
- package/dist/prompt.d.ts +22 -9
- package/dist/prompt.js +64 -48
- package/dist/prompt.js.map +1 -1
- package/dist/questions.e2e.test.js +13 -13
- package/dist/queue-sources.e2e.test.d.ts +1 -0
- package/dist/queue-sources.e2e.test.js +168 -0
- package/dist/queue-sources.e2e.test.js.map +1 -0
- package/dist/queue.d.ts +9 -1
- package/dist/queue.e2e.test.js +48 -30
- package/dist/queue.e2e.test.js.map +1 -1
- package/dist/queue.js +97 -24
- package/dist/queue.js.map +1 -1
- package/dist/queue.test.d.ts +1 -0
- package/dist/queue.test.js +57 -0
- package/dist/queue.test.js.map +1 -0
- package/dist/remote-send.e2e.test.js +6 -6
- package/dist/remote-send.js +2 -1
- package/dist/remote-send.js.map +1 -1
- package/dist/renderer.e2e.test.js +7 -7
- package/dist/restart.e2e.test.js +2 -2
- package/dist/routines.e2e.test.js +2 -2
- package/dist/scheduler.d.ts +3 -3
- package/dist/scheduler.e2e.test.js +43 -20
- package/dist/scheduler.e2e.test.js.map +1 -1
- package/dist/scheduler.js +4 -2
- package/dist/scheduler.js.map +1 -1
- package/dist/schema-sql.d.ts +1 -1
- package/dist/schema-sql.js +1 -1
- package/dist/schema-sql.js.map +1 -1
- package/dist/schema.d.ts +152 -6
- package/dist/schema.js +12 -0
- package/dist/schema.js.map +1 -1
- package/dist/session-commands.e2e.test.js +36 -11
- package/dist/session-commands.e2e.test.js.map +1 -1
- package/dist/session-events.d.ts +18 -6
- package/dist/session-events.js +175 -27
- package/dist/session-events.js.map +1 -1
- package/dist/session-ops.e2e.test.js +63 -12
- package/dist/session-ops.e2e.test.js.map +1 -1
- package/dist/sessions.d.ts +44 -21
- package/dist/sessions.js +177 -65
- package/dist/sessions.js.map +1 -1
- package/dist/shell.e2e.test.js +4 -4
- package/dist/slash-commands.js +6 -4
- package/dist/slash-commands.js.map +1 -1
- package/dist/slash-commands.test.js +2 -0
- package/dist/slash-commands.test.js.map +1 -1
- package/dist/sleep.e2e.test.js +29 -14
- package/dist/sleep.e2e.test.js.map +1 -1
- package/dist/sleeps.d.ts +2 -1
- package/dist/sleeps.js +21 -3
- package/dist/sleeps.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.js +3 -2
- package/dist/store.js.map +1 -1
- package/dist/system-prompt.d.ts +5 -15
- package/dist/system-prompt.js +180 -131
- package/dist/system-prompt.js.map +1 -1
- package/dist/test/generate-log-plugin/index.d.ts +3 -0
- package/dist/test/generate-log-plugin/index.js +15 -0
- package/dist/test/generate-log-plugin/index.js.map +1 -0
- package/dist/test/harness.d.ts +20 -2
- package/dist/test/harness.js +51 -11
- package/dist/test/harness.js.map +1 -1
- package/dist/test/replay.js +11 -8
- package/dist/test/replay.js.map +1 -1
- package/dist/thread-reducer.d.ts +29 -6
- package/dist/thread-reducer.fixtures.test.js +58 -51
- package/dist/thread-reducer.fixtures.test.js.map +1 -1
- package/dist/thread-reducer.js +109 -50
- package/dist/thread-reducer.js.map +1 -1
- package/dist/thread-reducer.test.js +190 -48
- package/dist/thread-reducer.test.js.map +1 -1
- package/dist/tunnel.d.ts +9 -0
- package/dist/tunnel.js +117 -28
- package/dist/tunnel.js.map +1 -1
- package/dist/tunnel.test.js +81 -5
- package/dist/tunnel.test.js.map +1 -1
- package/dist/voice-attachments.e2e.test.js +108 -18
- package/dist/voice-attachments.e2e.test.js.map +1 -1
- package/dist/voice-call.e2e.test.js +9 -9
- package/dist/voice-call.js +6 -6
- package/dist/voice-call.js.map +1 -1
- package/dist/voice-openai.e2e.test.js +59 -23
- package/dist/voice-openai.e2e.test.js.map +1 -1
- package/dist/voice.d.ts +1 -1
- package/dist/voice.js +72 -24
- package/dist/voice.js.map +1 -1
- package/dist/working-directory.e2e.test.js +11 -11
- package/dist/working-directory.e2e.test.js.map +1 -1
- package/package.json +11 -5
- package/skills/kimaki-tunnel/SKILL.md +3 -2
- package/src/agent-ui.e2e.test.ts +27 -13
- package/src/agent-ui.ts +2 -1
- package/src/analytics.e2e.test.ts +1 -1
- package/src/autostart.ts +55 -10
- package/src/bot.ts +36 -19
- package/src/btw.e2e.test.ts +3 -3
- package/src/builtin-skills.test.ts +73 -1
- package/src/builtin-skills.ts +33 -0
- package/src/catalog-refresh.e2e.test.ts +2 -2
- package/src/cli/bot.ts +30 -19
- package/src/cli/discord.ts +30 -6
- package/src/cli/schedule.ts +23 -9
- package/src/cli/session.ts +35 -12
- package/src/cli/shared.ts +43 -8
- package/src/cli-discord.e2e.test.ts +13 -8
- package/src/cli-lock.e2e.test.ts +88 -20
- package/src/cli-read.e2e.test.ts +17 -8
- package/src/cli-sessions.e2e.test.ts +4 -3
- package/src/cli.ts +2 -0
- package/src/commands/preference-commands.ts +38 -4
- package/src/commands/session-commands.ts +12 -7
- package/src/commands/thread-commands.ts +92 -24
- package/src/commands/worktree-commands.ts +10 -9
- package/src/commands.e2e.test.ts +25 -21
- package/src/coordinator-agents.e2e.test.ts +145 -0
- package/src/coordinator-idle.e2e.test.ts +122 -0
- package/src/coordinator-queue.e2e.test.ts +279 -0
- package/src/coordinator-warmup.e2e.test.ts +158 -0
- package/src/coordinator.e2e.test.ts +545 -0
- package/src/coordinator.ts +168 -0
- package/src/db.test.ts +1 -0
- package/src/diagram.test.ts +56 -0
- package/src/diagram.ts +243 -0
- package/src/duration.ts +13 -5
- package/src/effects.ts +99 -27
- package/src/event-loop.ts +71 -45
- package/src/format-parts.ts +53 -22
- package/src/gateway-replay.e2e.test.ts +4 -4
- package/src/harness.e2e.test.ts +2 -2
- package/src/hello-thread.e2e.test.ts +59 -5
- package/src/ingress.ts +44 -11
- package/src/interaction-context.ts +12 -11
- package/src/interrupt.e2e.test.ts +8 -5
- package/src/legacy.e2e.test.ts +1 -1
- package/src/lock-routes.ts +62 -8
- package/src/lock-server.ts +23 -7
- package/src/main.ts +26 -10
- package/src/markdown/markdown.test.ts +15 -4
- package/src/markdown/render-markdown.ts +9 -6
- package/src/onboarding-cli.e2e.test.ts +7 -6
- package/src/onboarding.e2e.test.ts +8 -10
- package/src/onboarding.ts +13 -29
- package/src/permissions.e2e.test.ts +3 -3
- package/src/plugin/coordinator.ts +401 -0
- package/src/plugin/index.ts +86 -59
- package/src/plugin.e2e.test.ts +1 -1
- package/src/preference-commands.e2e.test.ts +103 -23
- package/src/prompt.ts +87 -43
- package/src/questions.e2e.test.ts +13 -13
- package/src/queue-sources.e2e.test.ts +197 -0
- package/src/queue.e2e.test.ts +48 -30
- package/src/queue.test.ts +59 -0
- package/src/queue.ts +104 -24
- package/src/remote-send.e2e.test.ts +6 -6
- package/src/remote-send.ts +2 -1
- package/src/renderer.e2e.test.ts +7 -7
- package/src/restart.e2e.test.ts +2 -2
- package/src/routines.e2e.test.ts +2 -2
- package/src/scheduler.e2e.test.ts +45 -19
- package/src/scheduler.ts +4 -2
- package/src/schema-sql.ts +1 -1
- package/src/schema.ts +13 -0
- package/src/session-commands.e2e.test.ts +38 -11
- package/src/session-events.ts +168 -25
- package/src/session-ops.e2e.test.ts +64 -11
- package/src/sessions.ts +217 -67
- package/src/shell.e2e.test.ts +4 -4
- package/src/slash-commands.test.ts +2 -0
- package/src/slash-commands.ts +6 -4
- package/src/sleep.e2e.test.ts +30 -13
- package/src/sleeps.ts +20 -3
- package/src/store.ts +5 -2
- package/src/system-prompt.ts +190 -134
- package/src/test/generate-log-plugin/index.ts +16 -0
- package/src/test/harness.ts +77 -17
- package/src/test/replay.ts +12 -7
- package/src/thread-reducer.fixtures.test.ts +58 -51
- package/src/thread-reducer.test.ts +199 -49
- package/src/thread-reducer.ts +135 -66
- package/src/tunnel.test.ts +81 -5
- package/src/tunnel.ts +124 -25
- package/src/voice-attachments.e2e.test.ts +110 -18
- package/src/voice-call.e2e.test.ts +9 -9
- package/src/voice-call.ts +6 -6
- package/src/voice-openai.e2e.test.ts +64 -24
- package/src/voice.ts +93 -26
- package/src/working-directory.e2e.test.ts +11 -11
- package/src/message-tool.e2e.test.ts +0 -45
|
@@ -32,6 +32,7 @@ function reply(marker: string, text: string, extra: DeterministicMatcher['when']
|
|
|
32
32
|
const matchers: DeterministicMatcher[] = [
|
|
33
33
|
reply('new-marker', 'saw the file', { rawPromptIncludes: 'file-body-marker' }),
|
|
34
34
|
reply('first-marker', 'first answer'),
|
|
35
|
+
reply('empty-marker', 'started on the first message'),
|
|
35
36
|
reply('second-marker', 'second answer', {}, 510),
|
|
36
37
|
reply('tui-marker', 'tui answer'),
|
|
37
38
|
reply('resumed-marker', 'resumed answer'),
|
|
@@ -137,16 +138,42 @@ test('/new-session starts a thread with attached files and an agent', async () =
|
|
|
137
138
|
`)
|
|
138
139
|
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
139
140
|
"--- from: assistant (TestBot)
|
|
140
|
-
» **tommy:** Read it new-marker
|
|
141
|
-
Files: notes.txt
|
|
141
|
+
-# » **tommy:** Read it new-marker
|
|
142
|
+
-# Files: notes.txt
|
|
142
143
|
-# *using deterministic-provider/deterministic-v2 ⋅ plan*
|
|
143
144
|
saw the file
|
|
144
|
-
-# *
|
|
145
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2 ⋅ plan*"
|
|
145
146
|
`)
|
|
146
147
|
// A slash command thread has no start message: the user must be added explicitly.
|
|
147
148
|
expect(await discord.channel(thread.id).getMemberIds()).toContain(TEST_USER_ID)
|
|
148
149
|
})
|
|
149
150
|
|
|
151
|
+
test('/new-session without a prompt waits for the first thread message', async () => {
|
|
152
|
+
const { discord, channelId } = twin
|
|
153
|
+
const user = discord.channel(channelId).user(TEST_USER_ID)
|
|
154
|
+
const client = await server.client()
|
|
155
|
+
const thread = await newThread(() =>
|
|
156
|
+
user.runSlashCommand({ name: 'new-session', options: [{ name: 'agent', type: 3, value: 'plan' }] }),
|
|
157
|
+
)
|
|
158
|
+
await waitFor({ label: 'ready note', check: async () => (await discord.thread(thread.id).text()).includes('Send a message') })
|
|
159
|
+
const sessionId = bot.store.getState().roots[thread.id]
|
|
160
|
+
if (!sessionId) throw new Error('thread has no session')
|
|
161
|
+
expect((await client.message.list({ sessionID: sessionId, order: 'asc', limit: 10 })).data).toEqual([])
|
|
162
|
+
await discord.thread(thread.id).user(TEST_USER_ID).sendMessage({ content: 'Go empty-marker' })
|
|
163
|
+
await waitForFooter({ discord, threadId: thread.id })
|
|
164
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
165
|
+
"--- from: assistant (TestBot)
|
|
166
|
+
Send a message to start working.
|
|
167
|
+
--- from: user (tommy)
|
|
168
|
+
Go empty-marker
|
|
169
|
+
--- from: assistant (TestBot)
|
|
170
|
+
-# *using deterministic-provider/deterministic-v2 ⋅ plan*
|
|
171
|
+
started on the first message
|
|
172
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2 ⋅ plan*"
|
|
173
|
+
`)
|
|
174
|
+
expect(thread.name).toBe('Kimaki session')
|
|
175
|
+
})
|
|
176
|
+
|
|
150
177
|
test('/resume binds an existing session to a new thread and moves the binding', async () => {
|
|
151
178
|
const { discord, channelId } = twin
|
|
152
179
|
// A session started outside Discord (for example in the TUI).
|
|
@@ -180,10 +207,10 @@ test('/resume binds an existing session to a new thread and moves the binding',
|
|
|
180
207
|
Go on resumed-marker
|
|
181
208
|
--- from: assistant (TestBot)
|
|
182
209
|
resumed answer
|
|
183
|
-
-# *
|
|
184
|
-
» **user:** Back in the TUI tui-marker
|
|
210
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
211
|
+
-# » **user:** Back in the TUI tui-marker
|
|
185
212
|
tui answer
|
|
186
|
-
-# *
|
|
213
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
187
214
|
`)
|
|
188
215
|
|
|
189
216
|
// Resuming again moves the session: the old thread stops following it.
|
|
@@ -229,7 +256,7 @@ test('/fork forks before a user message into a new thread', async () => {
|
|
|
229
256
|
Check fork-check
|
|
230
257
|
--- from: assistant (TestBot)
|
|
231
258
|
fork has only the first message
|
|
232
|
-
-# *
|
|
259
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
233
260
|
`)
|
|
234
261
|
expect(hide(await discord.thread(source.id).text())).toMatchInlineSnapshot(`
|
|
235
262
|
"--- from: user (tommy)
|
|
@@ -237,12 +264,12 @@ test('/fork forks before a user message into a new thread', async () => {
|
|
|
237
264
|
--- from: assistant (TestBot)
|
|
238
265
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
239
266
|
first answer
|
|
240
|
-
-# *
|
|
267
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
241
268
|
--- from: user (tommy)
|
|
242
269
|
Then second-marker
|
|
243
270
|
--- from: assistant (TestBot)
|
|
244
271
|
second answer
|
|
245
|
-
-# *
|
|
272
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
246
273
|
Session forked! Continue in <#THREAD>"
|
|
247
274
|
`)
|
|
248
275
|
})
|
|
@@ -296,7 +323,7 @@ test('/fork-subagent forks a subagent session into a new thread', async () => {
|
|
|
296
323
|
More child-followup
|
|
297
324
|
--- from: assistant (TestBot)
|
|
298
325
|
child fork continues
|
|
299
|
-
-# *
|
|
326
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2 ⋅ general*"
|
|
300
327
|
`)
|
|
301
328
|
})
|
|
302
329
|
|
|
@@ -321,6 +348,6 @@ test('/resume of a session waiting on a question shows the question in the new t
|
|
|
321
348
|
Which color?
|
|
322
349
|
✓ _Red_
|
|
323
350
|
ok
|
|
324
|
-
-# *
|
|
351
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*"
|
|
325
352
|
`)
|
|
326
353
|
})
|
package/src/session-events.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import crypto from 'node:crypto'
|
|
15
15
|
import fs from 'node:fs'
|
|
16
16
|
import path from 'node:path'
|
|
17
|
-
import type { OpenCodeClient } from '@opencode/client'
|
|
17
|
+
import type { OpenCodeClient, V2Event } from '@opencode/client'
|
|
18
18
|
|
|
19
19
|
import type { KimakiDb } from './db.ts'
|
|
20
20
|
import { ConfigError, DbError, OpenCodeError } from './errors.ts'
|
|
@@ -107,12 +107,16 @@ export async function resolveSession({
|
|
|
107
107
|
return { threadId: row.thread_id, sessionId: row.session_id }
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
type Message = Awaited<ReturnType<OpenCodeClient['message']['list']>>['data'][number]
|
|
110
|
+
export type Message = Awaited<ReturnType<OpenCodeClient['message']['list']>>['data'][number]
|
|
111
111
|
type Session = Awaited<ReturnType<OpenCodeClient['session']['get']>>
|
|
112
112
|
|
|
113
|
+
// Long enough for most edits, commands and subagent prompts; huge writes stay short.
|
|
114
|
+
const TOOL_INPUT_LIMIT = 5000
|
|
115
|
+
|
|
113
116
|
function renderMessage(message: Message, options: { thinking?: boolean; verbose?: boolean }): string {
|
|
114
117
|
const time = new Date(message.time.created).toISOString()
|
|
115
|
-
if (message.type === 'user') return `##
|
|
118
|
+
if (message.type === 'user' || message.type === 'synthetic' || message.type === 'system') return `## ${message.type} ${time}\n\n${message.text}`
|
|
119
|
+
if (message.type === 'compaction' && message.status !== 'failed') return `## compaction ${time}\n\n${message.summary}`
|
|
116
120
|
if (message.type !== 'assistant') return `## ${message.type} ${time}\n\n${JSON.stringify(message)}`
|
|
117
121
|
const header = `## assistant ${time} (${message.agent}, ${message.model.providerID}/${message.model.id})`
|
|
118
122
|
const parts = message.content.map((part) => {
|
|
@@ -120,10 +124,12 @@ function renderMessage(message: Message, options: { thinking?: boolean; verbose?
|
|
|
120
124
|
if (part.type === 'reasoning') return options.thinking ? part.text : ''
|
|
121
125
|
if (part.type === 'tool') {
|
|
122
126
|
const state = part.state
|
|
123
|
-
const input = options.verbose ? JSON.stringify(state.input ?? {}) : JSON.stringify(state.input ?? {}).slice(0,
|
|
127
|
+
const input = options.verbose ? JSON.stringify(state.input ?? {}) : JSON.stringify(state.input ?? {}).slice(0, TOOL_INPUT_LIMIT)
|
|
124
128
|
const error = state.status === 'error' ? `\nerror: ${JSON.stringify(state).slice(0, 500)}` : ''
|
|
125
129
|
const output = options.verbose && state.status === 'completed' ? `\n${JSON.stringify(state.content)}` : ''
|
|
126
|
-
|
|
130
|
+
// Without --verbose tool outputs are hidden, but uploaded media stays findable.
|
|
131
|
+
const media = options.verbose ? '' : mediaBlock(uploadedMedia(part))
|
|
132
|
+
return `tool ${part.name} [${state.status}] ${input}${error}${output}${media && `\n${media}`}`
|
|
127
133
|
}
|
|
128
134
|
return ''
|
|
129
135
|
})
|
|
@@ -131,34 +137,139 @@ function renderMessage(message: Message, options: { thinking?: boolean; verbose?
|
|
|
131
137
|
return `${header}\n\n${parts.join('\n\n')}${finish}`
|
|
132
138
|
}
|
|
133
139
|
|
|
140
|
+
export function formatDuration(ms: number): string {
|
|
141
|
+
if (ms < 1_000) return '<1s'
|
|
142
|
+
const seconds = Math.floor(ms / 1_000)
|
|
143
|
+
if (seconds < 60) return `${seconds}s`
|
|
144
|
+
const minutes = Math.floor(seconds / 60)
|
|
145
|
+
if (minutes < 60) return `${minutes}m ${seconds % 60}s`
|
|
146
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatTokens(tokens: number): string {
|
|
150
|
+
return tokens < 1_000 ? String(tokens) : `${Math.round(tokens / 1_000)}k`
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export type SessionUsage = { contextTokens: number; runMs: number | null; endedAt: number }
|
|
154
|
+
|
|
155
|
+
// Context size of the latest step, length of the latest run and when it ended. Oldest-first input.
|
|
156
|
+
// Agents use it to judge if a follow-up would rewrite a big, expired prompt cache.
|
|
157
|
+
export function sessionUsage(messages: readonly Message[]): SessionUsage | null {
|
|
158
|
+
const lastIndex = messages.findLastIndex((message) => message.type === 'assistant')
|
|
159
|
+
const last = messages[lastIndex]
|
|
160
|
+
if (last?.type !== 'assistant') return null
|
|
161
|
+
const step = messages.findLast((message) => message.type === 'assistant' && !!message.tokens)
|
|
162
|
+
const tokens = step?.type === 'assistant' ? step.tokens : undefined
|
|
163
|
+
const contextTokens = tokens ? tokens.input + tokens.output + tokens.reasoning + tokens.cache.read + tokens.cache.write : 0
|
|
164
|
+
// A run starts at a user prompt. Synthetic items also arrive mid-run (subagent results), so they count only without one.
|
|
165
|
+
const before = messages.slice(0, lastIndex)
|
|
166
|
+
const start = before.findLast((message) => message.type === 'user') ?? before.findLast((message) => message.type === 'synthetic')
|
|
167
|
+
const endedAt = last.time.completed ?? last.time.created
|
|
168
|
+
return { contextTokens, runMs: start ? endedAt - start.time.created : null, endedAt }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function usageLine(usage: SessionUsage | null, now: number): string {
|
|
172
|
+
if (!usage) return ''
|
|
173
|
+
const run = usage.runMs === null ? '' : `, last run took ${formatDuration(usage.runMs)}`
|
|
174
|
+
return `\n\ncontext: ${formatTokens(usage.contextTokens)} tokens${run}, last active ${formatDuration(now - usage.endedAt)} ago (${new Date(usage.endedAt).toISOString()})`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The text of the last assistant message that has text, like OpenCode's
|
|
178
|
+
// subagent completion (session/subagent-completion.ts). Oldest-first input.
|
|
179
|
+
export function finalAnswer(messages: readonly Message[]): string | null {
|
|
180
|
+
for (const message of [...messages].reverse()) {
|
|
181
|
+
if (message.type === 'user') return null
|
|
182
|
+
if (message.type !== 'assistant') continue
|
|
183
|
+
const text = message.content.flatMap((part) => (part.type === 'text' && part.text.trim() ? [part.text.trim()] : [])).join('\n\n')
|
|
184
|
+
if (text) return text
|
|
185
|
+
}
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
type MessagePart = Extract<Message, { type: 'assistant' }>['content'][number]
|
|
190
|
+
|
|
191
|
+
const UPLOAD_RESULT_PREFIX = '{"uploaded":'
|
|
192
|
+
// `"url":"..."` of each file in that JSON line (upload in prompt.ts). A regex, not zod:
|
|
193
|
+
// this module loads on every CLI start. Discord URLs have no quotes or backslashes.
|
|
194
|
+
const UPLOAD_URL = /"url":"([^"\\]+)"/g
|
|
195
|
+
|
|
196
|
+
// Discord URLs of the files a `kimaki upload-to-discord` call posted, from the result
|
|
197
|
+
// line it printed into the shell output (the system prompt says never to pipe it).
|
|
198
|
+
// The session history holds them, so this needs no Discord read and survives restarts.
|
|
199
|
+
function uploadedMedia(part: MessagePart): string[] {
|
|
200
|
+
if (part.type !== 'tool' || part.state.status !== 'completed') return []
|
|
201
|
+
return part.state.content
|
|
202
|
+
.flatMap((content) => (content.type === 'text' ? content.text.split('\n') : []))
|
|
203
|
+
.filter((line) => line.startsWith(UPLOAD_RESULT_PREFIX))
|
|
204
|
+
.flatMap((line) => [...line.matchAll(UPLOAD_URL)].map((match) => match[1]!))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function mediaBlock(urls: readonly string[]): string {
|
|
208
|
+
return urls.length === 0 ? '' : `Media uploaded to Discord:\n${urls.map((url) => `- ${url}`).join('\n')}`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Inputs and the final answer of each turn, no tool calls: what agents read most often.
|
|
212
|
+
function renderAnswers({ session, messages, usage = '' }: { session: Pick<Session, 'id' | 'title' | 'location'>; messages: Message[]; usage?: string }): string {
|
|
213
|
+
const title = `# ${session.title} (${session.id})\n\ndirectory: ${session.location.directory}${usage}`
|
|
214
|
+
const blocks: string[] = []
|
|
215
|
+
let turn: Message[] = []
|
|
216
|
+
const flush = () => {
|
|
217
|
+
const answer = finalAnswer(turn)
|
|
218
|
+
const last = turn.findLast((message) => message.type === 'assistant')
|
|
219
|
+
const media = mediaBlock(turn.flatMap((message) => (message.type === 'assistant' ? message.content.flatMap(uploadedMedia) : [])))
|
|
220
|
+
if (answer && last?.type === 'assistant') blocks.push(`## assistant ${new Date(last.time.created).toISOString()} (${last.agent}, ${last.model.providerID}/${last.model.id})\n\n${answer}${media && `\n\n${media}`}`)
|
|
221
|
+
turn = []
|
|
222
|
+
}
|
|
223
|
+
for (const message of messages) {
|
|
224
|
+
if (message.type === 'assistant') {
|
|
225
|
+
turn.push(message)
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
flush()
|
|
229
|
+
if (message.type === 'user' || message.type === 'synthetic') blocks.push(`## ${message.type} ${new Date(message.time.created).toISOString()}\n\n${message.text}`)
|
|
230
|
+
if (message.type === 'compaction' && message.status !== 'failed') blocks.push(`## compaction ${new Date(message.time.created).toISOString()}\n\n${message.summary}`)
|
|
231
|
+
}
|
|
232
|
+
// A turn without an answer yet (running, or waiting on a question): show its tool calls.
|
|
233
|
+
if (turn.length > 0 && !finalAnswer(turn)) blocks.push(...turn.map((message) => renderMessage(message, {})))
|
|
234
|
+
else flush()
|
|
235
|
+
const more = `Only prompts and final answers are shown. Every tool call and the reasoning: kimaki session read ${session.id} --thinking`
|
|
236
|
+
return [title, ...blocks, more].join('\n\n')
|
|
237
|
+
}
|
|
238
|
+
|
|
134
239
|
// Oldest first, as markdown. OpenCode returns the newest first.
|
|
240
|
+
// Default: prompts and final answers. `full`: every message with tool calls.
|
|
135
241
|
export async function readSessionMarkdown({
|
|
136
242
|
client,
|
|
137
243
|
sessionId,
|
|
244
|
+
full,
|
|
138
245
|
thinking,
|
|
139
246
|
verbose,
|
|
140
247
|
}: {
|
|
141
248
|
client: OpenCodeClient
|
|
142
249
|
sessionId: string
|
|
250
|
+
full?: boolean
|
|
143
251
|
thinking?: boolean
|
|
144
252
|
verbose?: boolean
|
|
145
253
|
}): Promise<OpenCodeError | string> {
|
|
146
|
-
const session = await
|
|
147
|
-
.get({ sessionID: sessionId })
|
|
148
|
-
|
|
254
|
+
const [session, messages] = await Promise.all([
|
|
255
|
+
client.session.get({ sessionID: sessionId }).catch((e) => new OpenCodeError({ operation: 'session.get', cause: e })),
|
|
256
|
+
allMessages({ client, sessionId }),
|
|
257
|
+
])
|
|
149
258
|
if (session instanceof Error) return session
|
|
150
|
-
const messages = await allMessages({ client, sessionId })
|
|
151
259
|
if (messages instanceof Error) return messages
|
|
152
|
-
|
|
260
|
+
const usage = usageLine(sessionUsage(messages), Date.now())
|
|
261
|
+
if (!full && !thinking && !verbose) return renderAnswers({ session, messages, usage })
|
|
262
|
+
return renderSession({ session, messages, thinking, verbose, usage })
|
|
153
263
|
}
|
|
154
264
|
|
|
155
|
-
|
|
156
|
-
|
|
265
|
+
// Transcripts pass no usage: their "ago" would be stale on disk.
|
|
266
|
+
function renderSession({ session, messages, thinking, verbose, usage = '' }: { session: Pick<Session, 'id' | 'title' | 'location'>; messages: Message[]; thinking?: boolean; verbose?: boolean; usage?: string }): string {
|
|
267
|
+
const title = `# ${session.title} (${session.id})\n\ndirectory: ${session.location.directory}${usage}`
|
|
157
268
|
return [title, ...messages.map((message) => renderMessage(message, { thinking, verbose }))].join('\n\n')
|
|
158
269
|
}
|
|
159
270
|
|
|
160
271
|
// Transcripts: <dataDir>/transcripts/<sessionId>.md, the `session read` markdown
|
|
161
|
-
//
|
|
272
|
+
// with thinking but no tool outputs, so agents can search every session with rg.
|
|
162
273
|
// The file mtime is the session's time.updated: a file at least that new is current.
|
|
163
274
|
export function transcriptsDir({ dataDir }: { dataDir: string }): string {
|
|
164
275
|
return path.join(dataDir, 'transcripts')
|
|
@@ -172,7 +283,7 @@ export async function writeTranscript({ client, dataDir, session }: { client: Op
|
|
|
172
283
|
const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`
|
|
173
284
|
const written = await (async () => {
|
|
174
285
|
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 })
|
|
175
|
-
await fs.promises.writeFile(temp, `${renderSession({ session, messages })}\n`, { mode: 0o600 })
|
|
286
|
+
await fs.promises.writeFile(temp, `${renderSession({ session, messages, thinking: true })}\n`, { mode: 0o600 })
|
|
176
287
|
await fs.promises.utimes(temp, session.time.updated / 1000, session.time.updated / 1000)
|
|
177
288
|
await fs.promises.rename(temp, file)
|
|
178
289
|
})().catch((cause) => new ConfigError({ reason: `Cannot write ${file}`, cause }))
|
|
@@ -260,14 +371,34 @@ export function allMessages({ client, sessionId, type }: { client: OpenCodeClien
|
|
|
260
371
|
})
|
|
261
372
|
}
|
|
262
373
|
|
|
263
|
-
export function allSessions({ client,
|
|
374
|
+
export function allSessions({ client, project }: { client: OpenCodeClient; project?: string }) {
|
|
264
375
|
return collectPages({
|
|
265
376
|
operation: 'session.list',
|
|
266
|
-
page: (cursor) => client.session.list({
|
|
377
|
+
page: (cursor) => client.session.list({ project, limit: 200, ...(cursor ? { cursor } : { order: 'desc' as const }) }),
|
|
267
378
|
})
|
|
268
379
|
}
|
|
269
380
|
|
|
270
|
-
|
|
381
|
+
// Why `kimaki session wait` returned for a session. `idle`: no run was in progress
|
|
382
|
+
// when the wait started. `running`: the timeout ended the wait first.
|
|
383
|
+
export type WaitStatus = 'finished' | 'failed' | 'interrupted' | 'idle' | 'question' | 'permission' | 'running'
|
|
384
|
+
|
|
385
|
+
function eventStatus(event: V2Event): { sessionId: string; status: WaitStatus } | null {
|
|
386
|
+
if (event.type === 'form.created') return { sessionId: event.data.form.sessionID, status: 'question' }
|
|
387
|
+
if (event.type === 'permission.asked') return { sessionId: event.data.sessionID, status: 'permission' }
|
|
388
|
+
if (event.type === 'session.execution.succeeded') return { sessionId: event.data.sessionID, status: 'finished' }
|
|
389
|
+
if (event.type === 'session.execution.failed') return { sessionId: event.data.sessionID, status: 'failed' }
|
|
390
|
+
if (event.type === 'session.execution.interrupted') return { sessionId: event.data.sessionID, status: 'interrupted' }
|
|
391
|
+
return null
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Waits until every session ended its run or paused for the user, on one event stream.
|
|
395
|
+
// When `signal` ends the wait, the sessions not done yet get `running`.
|
|
396
|
+
export async function waitForSessions({ client, sessionIds, signal }: { client: OpenCodeClient; sessionIds: readonly string[]; signal?: AbortSignal }): Promise<OpenCodeError | Map<string, WaitStatus>> {
|
|
397
|
+
const statuses = new Map<string, WaitStatus>()
|
|
398
|
+
const settle = (sessionId: string, status: WaitStatus) => {
|
|
399
|
+
if (sessionIds.includes(sessionId) && !statuses.has(sessionId)) statuses.set(sessionId, status)
|
|
400
|
+
}
|
|
401
|
+
const done = () => statuses.size === new Set(sessionIds).size
|
|
271
402
|
const controller = new AbortController()
|
|
272
403
|
const subscriptionSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal
|
|
273
404
|
const iterator = client.event.subscribe({ signal: subscriptionSignal })[Symbol.asyncIterator]()
|
|
@@ -275,27 +406,39 @@ export async function waitForSessionReady({ client, sessionId, signal }: { clien
|
|
|
275
406
|
const first = await iterator.next().catch((cause) => new OpenCodeError({ operation: 'event.subscribe', cause }))
|
|
276
407
|
if (first instanceof Error) return first
|
|
277
408
|
if (first.done || first.value.type !== 'server.connected') return new OpenCodeError({ operation: 'connect session wait stream' })
|
|
409
|
+
// Subscribed before hydrating, so no run can end unseen in between.
|
|
278
410
|
const consume = (async () => {
|
|
279
411
|
while (!subscriptionSignal.aborted) {
|
|
280
412
|
const next = await iterator.next().catch((cause) => new OpenCodeError({ operation: 'event.subscribe', cause }))
|
|
281
413
|
if (next instanceof Error) return next
|
|
282
414
|
if (next.done) return new OpenCodeError({ operation: 'session wait stream ended' })
|
|
283
|
-
const
|
|
284
|
-
if (
|
|
285
|
-
if (
|
|
286
|
-
if ((event.type === 'session.execution.succeeded' || event.type === 'session.execution.failed' || event.type === 'session.execution.interrupted') && event.data.sessionID === sessionId) return
|
|
415
|
+
const ended = eventStatus(next.value)
|
|
416
|
+
if (ended) settle(ended.sessionId, ended.status)
|
|
417
|
+
if (done()) return
|
|
287
418
|
}
|
|
288
419
|
return new OpenCodeError({ operation: 'session wait cancelled' })
|
|
289
420
|
})()
|
|
290
421
|
const hydrated = await Promise.all([
|
|
291
|
-
client.session.active(
|
|
422
|
+
client.session.active({ signal: subscriptionSignal }),
|
|
423
|
+
Promise.all(sessionIds.map((sessionID) => Promise.all([
|
|
424
|
+
client.session.form.list({ sessionID }, { signal: subscriptionSignal }),
|
|
425
|
+
client.permission.list({ sessionID }, { signal: subscriptionSignal }),
|
|
426
|
+
]))),
|
|
292
427
|
]).catch((cause) => new OpenCodeError({ operation: 'hydrate session wait', cause }))
|
|
293
428
|
if (hydrated instanceof Error) return hydrated
|
|
294
|
-
const [active,
|
|
295
|
-
|
|
429
|
+
const [active, pending] = hydrated
|
|
430
|
+
for (const [index, sessionId] of sessionIds.entries()) {
|
|
431
|
+
const [forms, permissions] = pending[index] ?? [[], []]
|
|
432
|
+
if (forms.length > 0) settle(sessionId, 'question')
|
|
433
|
+
if (permissions.length > 0) settle(sessionId, 'permission')
|
|
434
|
+
if (!active[sessionId]) settle(sessionId, 'idle')
|
|
435
|
+
}
|
|
436
|
+
if (done()) return
|
|
296
437
|
return consume
|
|
297
438
|
})()
|
|
298
439
|
controller.abort()
|
|
299
440
|
await iterator.return?.(undefined).catch(() => undefined)
|
|
300
|
-
return result
|
|
441
|
+
if (result instanceof Error && !signal?.aborted) return result
|
|
442
|
+
for (const sessionId of sessionIds) settle(sessionId, 'running')
|
|
443
|
+
return statuses
|
|
301
444
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
startTwin,
|
|
17
17
|
tempDataDir,
|
|
18
18
|
textParts,
|
|
19
|
+
waitFor,
|
|
19
20
|
waitForBotMessageContaining,
|
|
20
21
|
waitForFooter,
|
|
21
22
|
warmUp,
|
|
@@ -32,6 +33,21 @@ const matchers: DeterministicMatcher[] = [
|
|
|
32
33
|
then: { parts: textParts('## Objective\n- Test compaction') },
|
|
33
34
|
},
|
|
34
35
|
{ id: 'usage', priority: 500, when: { latestUserTextIncludes: 'usage-marker' }, then: { parts: textParts('counted') } },
|
|
36
|
+
// A reasoning block that stays open long enough to run /context-usage inside it.
|
|
37
|
+
{
|
|
38
|
+
id: 'slow-thinking',
|
|
39
|
+
priority: 500,
|
|
40
|
+
when: { latestUserTextIncludes: 'thinking-marker' },
|
|
41
|
+
then: {
|
|
42
|
+
parts: [
|
|
43
|
+
{ type: 'reasoning-start', id: 'r1' },
|
|
44
|
+
{ type: 'reasoning-delta', id: 'r1', delta: 'pondering' },
|
|
45
|
+
{ type: 'reasoning-end', id: 'r1' },
|
|
46
|
+
...textParts('thought it through'),
|
|
47
|
+
],
|
|
48
|
+
partDelaysMs: [0, 0, 2_500, 0, 0, 0, 0],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
35
51
|
...scriptedTurn({
|
|
36
52
|
marker: 'write-marker',
|
|
37
53
|
steps: [{ id: 'call-write-1', tool: 'write', input: { path: 'undo.txt', content: 'written by the agent\n' } }],
|
|
@@ -75,40 +91,77 @@ async function command(threadId: string, name: string, text: string) {
|
|
|
75
91
|
return waitForBotMessageContaining({ discord: twin.discord, threadId, text })
|
|
76
92
|
}
|
|
77
93
|
|
|
94
|
+
// /context-usage status durations depend on timing.
|
|
95
|
+
function stableDurations(text: string) {
|
|
96
|
+
return text.replace(/<1s|\b\d+m \d+s\b|\b\d+s\b/g, 'Ns')
|
|
97
|
+
}
|
|
98
|
+
|
|
78
99
|
test('/diff with a clean working tree and /context-usage after a turn', async () => {
|
|
79
100
|
const thread = await newThread('Hello usage-marker')
|
|
80
101
|
await command(thread.id, 'diff', 'No changes')
|
|
81
102
|
await command(thread.id, 'context-usage', 'Context usage')
|
|
82
|
-
expect(await twin.discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
103
|
+
expect(stableDurations(await twin.discord.thread(thread.id).text())).toMatchInlineSnapshot(`
|
|
83
104
|
"--- from: user (tommy)
|
|
84
105
|
Hello usage-marker
|
|
85
106
|
--- from: assistant (TestBot)
|
|
86
107
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
87
108
|
counted
|
|
88
|
-
-# *
|
|
109
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
89
110
|
No changes to show
|
|
90
111
|
**Context usage:** 0%, 2 / 200,000 tokens
|
|
91
|
-
**Model:** deterministic-provider/deterministic-v2
|
|
112
|
+
**Model:** deterministic-provider/deterministic-v2
|
|
113
|
+
**Status:** idle for Ns"
|
|
114
|
+
`)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('/context-usage while the model thinks shows the run and what it does', async () => {
|
|
118
|
+
const { discord, channelId } = twin
|
|
119
|
+
const before = new Set((await discord.channel(channelId).getThreads()).map((thread) => thread.id))
|
|
120
|
+
await discord.channel(channelId).user(TEST_USER_ID).sendMessage({ content: 'Ponder this thinking-marker' })
|
|
121
|
+
const thread = await discord.channel(channelId).waitForThread({ timeout: 8_000, predicate: (candidate) => !before.has(candidate.id) })
|
|
122
|
+
const client = await server.client()
|
|
123
|
+
await waitFor({
|
|
124
|
+
label: 'open reasoning block',
|
|
125
|
+
check: async () => {
|
|
126
|
+
const sessionId = bot.store.getState().roots[thread.id]
|
|
127
|
+
if (!sessionId) return null
|
|
128
|
+
const messages = await client.message.list({ sessionID: sessionId, type: 'assistant', order: 'desc', limit: 1 })
|
|
129
|
+
return messages.data.some((message) => message.type === 'assistant' && message.content.some((part) => part.type === 'reasoning')) || null
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
await command(thread.id, 'context-usage', 'Status:')
|
|
133
|
+
await waitForFooter({ discord, threadId: thread.id })
|
|
134
|
+
expect(stableDurations(await discord.thread(thread.id).text())).toMatchInlineSnapshot(`
|
|
135
|
+
"--- from: user (tommy)
|
|
136
|
+
Ponder this thinking-marker
|
|
137
|
+
--- from: assistant (TestBot)
|
|
138
|
+
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
139
|
+
Token usage not available for this session yet
|
|
140
|
+
**Status:** running for Ns, thinking for Ns
|
|
141
|
+
**Last activity:** Ns ago, thinking
|
|
142
|
+
thought it through
|
|
143
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
92
144
|
`)
|
|
93
145
|
})
|
|
94
146
|
|
|
95
147
|
test('/compact summarizes the history without a footer', async () => {
|
|
96
148
|
const thread = await newThread('Something to compact')
|
|
97
149
|
await command(thread.id, 'compact', 'Compacting')
|
|
98
|
-
|
|
150
|
+
const sessionId = bot.store.getState().roots[thread.id]!
|
|
151
|
+
const client = await server.client()
|
|
152
|
+
await waitFor({
|
|
153
|
+
label: 'compaction message',
|
|
154
|
+
check: async () => (await client.message.list({ sessionID: sessionId, order: 'asc' })).data.some((message) => message.type === 'compaction') || null,
|
|
155
|
+
})
|
|
99
156
|
expect(await twin.discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
100
157
|
"--- from: user (tommy)
|
|
101
158
|
Something to compact
|
|
102
159
|
--- from: assistant (TestBot)
|
|
103
160
|
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
104
161
|
ok
|
|
105
|
-
-# *
|
|
106
|
-
Compacting the session context
|
|
107
|
-
-# ⬦ context compacted"
|
|
162
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*
|
|
163
|
+
Compacting the session context"
|
|
108
164
|
`)
|
|
109
|
-
const sessionId = bot.store.getState().roots[thread.id]!
|
|
110
|
-
const messages = await (await server.client()).message.list({ sessionID: sessionId, order: 'asc' })
|
|
111
|
-
expect(messages.data.map((message) => message.type)).toContain('compaction')
|
|
112
165
|
})
|
|
113
166
|
|
|
114
167
|
test('/undo hides the last turn but keeps its files, /redo restores the turn', async () => {
|
|
@@ -133,7 +186,7 @@ test('/undo hides the last turn but keeps its files, /redo restores the turn', a
|
|
|
133
186
|
-# ◼︎ write *undo.txt* (2 lines)
|
|
134
187
|
|
|
135
188
|
wrote undo.txt
|
|
136
|
-
-# *
|
|
189
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
137
190
|
Undone - removed the last turn from the session. File changes were kept
|
|
138
191
|
Restored - session fully back to its previous state
|
|
139
192
|
Nothing to redo - no previous undo found"
|