kimaki 1.4.0 → 1.6.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 +10 -1
- package/dist/bot.js +14 -6
- package/dist/bot.js.map +1 -1
- package/dist/btw.e2e.test.js +3 -3
- package/dist/builtin-skills.d.ts +1 -1
- package/dist/builtin-skills.js +1 -1
- package/dist/builtin-skills.js.map +1 -1
- package/dist/builtin-skills.test.js +3 -4
- package/dist/builtin-skills.test.js.map +1 -1
- package/dist/catalog-refresh.e2e.test.js +2 -2
- package/dist/cli/bot.js +25 -14
- 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 +4 -5
- 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.js +135 -0
- package/dist/coordinator-agents.e2e.test.js.map +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.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 +75 -0
- 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 +1 -1
- package/dist/event-loop.js +73 -43
- 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 +68 -13
- 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 +68 -16
- 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 +4 -2
- package/dist/main.js +13 -3
- package/dist/main.js.map +1 -1
- package/dist/markdown/markdown.test.js +4 -4
- package/dist/markdown/render-markdown.js +6 -5
- 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 +76 -17
- package/dist/plugin/index.js.map +1 -1
- 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/prompt.d.ts +21 -8
- 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 +1 -1
- 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 +146 -0
- 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 +19 -7
- package/dist/session-events.js +166 -22
- package/dist/session-events.js.map +1 -1
- package/dist/session-ops.e2e.test.js +88 -11
- package/dist/session-ops.e2e.test.js.map +1 -1
- package/dist/sessions.d.ts +41 -18
- package/dist/sessions.js +174 -63
- package/dist/sessions.js.map +1 -1
- package/dist/shell.e2e.test.js +4 -4
- package/dist/slash-commands.js +12 -6
- 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 +1 -0
- 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 +3 -1
- package/dist/system-prompt.js +138 -64
- package/dist/system-prompt.js.map +1 -1
- 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 -1
- package/dist/test/harness.js +51 -10
- package/dist/test/harness.js.map +1 -1
- package/dist/test/replay.js +11 -7
- package/dist/test/replay.js.map +1 -1
- package/dist/thread-reducer.d.ts +29 -3
- package/dist/thread-reducer.fixtures.test.js +58 -51
- package/dist/thread-reducer.fixtures.test.js.map +1 -1
- package/dist/thread-reducer.js +112 -25
- package/dist/thread-reducer.js.map +1 -1
- package/dist/thread-reducer.test.js +197 -24
- package/dist/thread-reducer.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.js +80 -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 +12 -6
- 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 +29 -5
- package/src/btw.e2e.test.ts +3 -3
- package/src/builtin-skills.test.ts +3 -4
- package/src/builtin-skills.ts +1 -1
- package/src/catalog-refresh.e2e.test.ts +2 -2
- package/src/cli/bot.ts +23 -14
- 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 +168 -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 -42
- 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 +62 -12
- 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 +22 -7
- package/src/markdown/markdown.test.ts +4 -4
- package/src/markdown/render-markdown.ts +6 -5
- 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 +68 -15
- 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 +160 -21
- package/src/session-ops.e2e.test.ts +90 -10
- package/src/sessions.ts +214 -64
- package/src/shell.e2e.test.ts +4 -4
- package/src/slash-commands.test.ts +2 -0
- package/src/slash-commands.ts +12 -6
- 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 +143 -63
- package/src/test/generate-log-plugin/index.ts +16 -0
- package/src/test/harness.ts +77 -14
- package/src/test/replay.ts +12 -6
- package/src/thread-reducer.fixtures.test.ts +58 -51
- package/src/thread-reducer.test.ts +207 -26
- package/src/thread-reducer.ts +138 -37
- 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 +101 -26
- package/src/working-directory.e2e.test.ts +11 -11
- package/dist/message-tool.e2e.test.js +0 -39
- package/dist/message-tool.e2e.test.js.map +0 -1
- package/dist/plugin/read-media.d.ts +0 -23
- package/dist/plugin/read-media.js +0 -72
- package/dist/plugin/read-media.js.map +0 -1
- package/dist/plugin/session-id/index.js +0 -18
- package/dist/plugin/session-id/index.js.map +0 -1
- package/dist/probe-live.tmp.js +0 -82
- package/dist/probe-live.tmp.js.map +0 -1
- package/dist/tasks.e2e.test.js +0 -173
- package/dist/tasks.e2e.test.js.map +0 -1
- /package/dist/{message-tool.e2e.test.d.ts → coordinator-agents.e2e.test.d.ts} +0 -0
- /package/dist/{probe-live.tmp.d.ts → coordinator-idle.e2e.test.d.ts} +0 -0
- /package/dist/{tasks.e2e.test.d.ts → coordinator-queue.e2e.test.d.ts} +0 -0
- /package/dist/{plugin/session-id → test/generate-log-plugin}/index.d.ts +0 -0
|
@@ -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,7 +107,7 @@ 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
113
|
// Long enough for most edits, commands and subagent prompts; huge writes stay short.
|
|
@@ -127,7 +127,9 @@ function renderMessage(message: Message, options: { thinking?: boolean; verbose?
|
|
|
127
127
|
const input = options.verbose ? JSON.stringify(state.input ?? {}) : JSON.stringify(state.input ?? {}).slice(0, TOOL_INPUT_LIMIT)
|
|
128
128
|
const error = state.status === 'error' ? `\nerror: ${JSON.stringify(state).slice(0, 500)}` : ''
|
|
129
129
|
const output = options.verbose && state.status === 'completed' ? `\n${JSON.stringify(state.content)}` : ''
|
|
130
|
-
|
|
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}`}`
|
|
131
133
|
}
|
|
132
134
|
return ''
|
|
133
135
|
})
|
|
@@ -135,29 +137,134 @@ function renderMessage(message: Message, options: { thinking?: boolean; verbose?
|
|
|
135
137
|
return `${header}\n\n${parts.join('\n\n')}${finish}`
|
|
136
138
|
}
|
|
137
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
|
+
|
|
138
239
|
// Oldest first, as markdown. OpenCode returns the newest first.
|
|
240
|
+
// Default: prompts and final answers. `full`: every message with tool calls.
|
|
139
241
|
export async function readSessionMarkdown({
|
|
140
242
|
client,
|
|
141
243
|
sessionId,
|
|
244
|
+
full,
|
|
142
245
|
thinking,
|
|
143
246
|
verbose,
|
|
144
247
|
}: {
|
|
145
248
|
client: OpenCodeClient
|
|
146
249
|
sessionId: string
|
|
250
|
+
full?: boolean
|
|
147
251
|
thinking?: boolean
|
|
148
252
|
verbose?: boolean
|
|
149
253
|
}): Promise<OpenCodeError | string> {
|
|
150
|
-
const session = await
|
|
151
|
-
.get({ sessionID: sessionId })
|
|
152
|
-
|
|
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
|
+
])
|
|
153
258
|
if (session instanceof Error) return session
|
|
154
|
-
const messages = await allMessages({ client, sessionId })
|
|
155
259
|
if (messages instanceof Error) return messages
|
|
156
|
-
|
|
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 })
|
|
157
263
|
}
|
|
158
264
|
|
|
159
|
-
|
|
160
|
-
|
|
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}`
|
|
161
268
|
return [title, ...messages.map((message) => renderMessage(message, { thinking, verbose }))].join('\n\n')
|
|
162
269
|
}
|
|
163
270
|
|
|
@@ -264,14 +371,34 @@ export function allMessages({ client, sessionId, type }: { client: OpenCodeClien
|
|
|
264
371
|
})
|
|
265
372
|
}
|
|
266
373
|
|
|
267
|
-
export function allSessions({ client,
|
|
374
|
+
export function allSessions({ client, project }: { client: OpenCodeClient; project?: string }) {
|
|
268
375
|
return collectPages({
|
|
269
376
|
operation: 'session.list',
|
|
270
|
-
page: (cursor) => client.session.list({
|
|
377
|
+
page: (cursor) => client.session.list({ project, limit: 200, ...(cursor ? { cursor } : { order: 'desc' as const }) }),
|
|
271
378
|
})
|
|
272
379
|
}
|
|
273
380
|
|
|
274
|
-
|
|
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
|
|
275
402
|
const controller = new AbortController()
|
|
276
403
|
const subscriptionSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal
|
|
277
404
|
const iterator = client.event.subscribe({ signal: subscriptionSignal })[Symbol.asyncIterator]()
|
|
@@ -279,27 +406,39 @@ export async function waitForSessionReady({ client, sessionId, signal }: { clien
|
|
|
279
406
|
const first = await iterator.next().catch((cause) => new OpenCodeError({ operation: 'event.subscribe', cause }))
|
|
280
407
|
if (first instanceof Error) return first
|
|
281
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.
|
|
282
410
|
const consume = (async () => {
|
|
283
411
|
while (!subscriptionSignal.aborted) {
|
|
284
412
|
const next = await iterator.next().catch((cause) => new OpenCodeError({ operation: 'event.subscribe', cause }))
|
|
285
413
|
if (next instanceof Error) return next
|
|
286
414
|
if (next.done) return new OpenCodeError({ operation: 'session wait stream ended' })
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
289
|
-
if (
|
|
290
|
-
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
|
|
291
418
|
}
|
|
292
419
|
return new OpenCodeError({ operation: 'session wait cancelled' })
|
|
293
420
|
})()
|
|
294
421
|
const hydrated = await Promise.all([
|
|
295
|
-
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
|
+
]))),
|
|
296
427
|
]).catch((cause) => new OpenCodeError({ operation: 'hydrate session wait', cause }))
|
|
297
428
|
if (hydrated instanceof Error) return hydrated
|
|
298
|
-
const [active,
|
|
299
|
-
|
|
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
|
|
300
437
|
return consume
|
|
301
438
|
})()
|
|
302
439
|
controller.abort()
|
|
303
440
|
await iterator.return?.(undefined).catch(() => undefined)
|
|
304
|
-
return result
|
|
441
|
+
if (result instanceof Error && !signal?.aborted) return result
|
|
442
|
+
for (const sessionId of sessionIds) settle(sessionId, 'running')
|
|
443
|
+
return statuses
|
|
305
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,104 @@ 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
|
`)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
// Issue #258: the rename's CHANNEL_NAME_CHANGE message holds only the new name.
|
|
168
|
+
test('renaming the thread in Discord sets the session title without a prompt', async () => {
|
|
169
|
+
const thread = await newThread('Hello rename-marker')
|
|
109
170
|
const sessionId = bot.store.getState().roots[thread.id]!
|
|
110
|
-
const
|
|
111
|
-
|
|
171
|
+
const client = await server.client()
|
|
172
|
+
const requests = server.model.requests.length
|
|
173
|
+
await twin.discord.thread(thread.id).user(TEST_USER_ID).renameThread({ name: 'Renamed by hand' })
|
|
174
|
+
await waitFor({
|
|
175
|
+
label: 'session title from the thread name',
|
|
176
|
+
check: async () => (await client.session.get({ sessionID: sessionId })).title === 'Renamed by hand' || null,
|
|
177
|
+
})
|
|
178
|
+
for (let i = 0; i < 10; i++) {
|
|
179
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
180
|
+
expect(server.model.requests.length).toBe(requests)
|
|
181
|
+
}
|
|
182
|
+
expect(await twin.discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
183
|
+
"--- from: user (tommy)
|
|
184
|
+
Hello rename-marker
|
|
185
|
+
--- from: assistant (TestBot)
|
|
186
|
+
-# *using deterministic-provider/deterministic-v2 ⋅ build*
|
|
187
|
+
ok
|
|
188
|
+
-# *main ⋅ Ns ⋅ deterministic-v2*
|
|
189
|
+
--- from: user (tommy)
|
|
190
|
+
[renamed thread to: Renamed by hand]"
|
|
191
|
+
`)
|
|
112
192
|
})
|
|
113
193
|
|
|
114
194
|
test('/undo hides the last turn but keeps its files, /redo restores the turn', async () => {
|
|
@@ -133,7 +213,7 @@ test('/undo hides the last turn but keeps its files, /redo restores the turn', a
|
|
|
133
213
|
-# ◼︎ write *undo.txt* (2 lines)
|
|
134
214
|
|
|
135
215
|
wrote undo.txt
|
|
136
|
-
-# *
|
|
216
|
+
-# *main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
137
217
|
Undone - removed the last turn from the session. File changes were kept
|
|
138
218
|
Restored - session fully back to its previous state
|
|
139
219
|
Nothing to redo - no previous undo found"
|