kimaki 0.4.41 → 0.4.43

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/cli.js CHANGED
@@ -1027,23 +1027,68 @@ cli
1027
1027
  throw new Error(`Channel belongs to a different bot (expected: ${appId}, got: ${channelAppId})`);
1028
1028
  }
1029
1029
  s.message('Creating starter message...');
1030
- // Create starter message with just the prompt (no prefix)
1031
- const starterMessageResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
1032
- method: 'POST',
1033
- headers: {
1034
- Authorization: `Bot ${botToken}`,
1035
- 'Content-Type': 'application/json',
1036
- },
1037
- body: JSON.stringify({
1038
- content: prompt,
1039
- }),
1040
- });
1041
- if (!starterMessageResponse.ok) {
1042
- const error = await starterMessageResponse.text();
1043
- s.stop('Failed to create message');
1044
- throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`);
1030
+ // Discord has a 2000 character limit for messages.
1031
+ // If prompt exceeds this, send it as a file attachment instead.
1032
+ const DISCORD_MAX_LENGTH = 2000;
1033
+ let starterMessage;
1034
+ if (prompt.length > DISCORD_MAX_LENGTH) {
1035
+ // Send as file attachment with a short summary
1036
+ const preview = prompt.slice(0, 100).replace(/\n/g, ' ');
1037
+ const summaryContent = `📄 **Prompt attached as file** (${prompt.length} chars)\n\n> ${preview}...`;
1038
+ // Write prompt to a temp file
1039
+ const tmpDir = path.join(process.cwd(), 'tmp');
1040
+ if (!fs.existsSync(tmpDir)) {
1041
+ fs.mkdirSync(tmpDir, { recursive: true });
1042
+ }
1043
+ const tmpFile = path.join(tmpDir, `prompt-${Date.now()}.md`);
1044
+ fs.writeFileSync(tmpFile, prompt);
1045
+ try {
1046
+ // Create message with file attachment
1047
+ const formData = new FormData();
1048
+ formData.append('payload_json', JSON.stringify({
1049
+ content: summaryContent,
1050
+ attachments: [{ id: 0, filename: 'prompt.md' }],
1051
+ }));
1052
+ const buffer = fs.readFileSync(tmpFile);
1053
+ formData.append('files[0]', new Blob([buffer], { type: 'text/markdown' }), 'prompt.md');
1054
+ const starterMessageResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
1055
+ method: 'POST',
1056
+ headers: {
1057
+ Authorization: `Bot ${botToken}`,
1058
+ },
1059
+ body: formData,
1060
+ });
1061
+ if (!starterMessageResponse.ok) {
1062
+ const error = await starterMessageResponse.text();
1063
+ s.stop('Failed to create message');
1064
+ throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`);
1065
+ }
1066
+ starterMessage = (await starterMessageResponse.json());
1067
+ }
1068
+ finally {
1069
+ // Clean up temp file
1070
+ fs.unlinkSync(tmpFile);
1071
+ }
1072
+ }
1073
+ else {
1074
+ // Normal case: send prompt inline
1075
+ const starterMessageResponse = await fetch(`https://discord.com/api/v10/channels/${channelId}/messages`, {
1076
+ method: 'POST',
1077
+ headers: {
1078
+ Authorization: `Bot ${botToken}`,
1079
+ 'Content-Type': 'application/json',
1080
+ },
1081
+ body: JSON.stringify({
1082
+ content: prompt,
1083
+ }),
1084
+ });
1085
+ if (!starterMessageResponse.ok) {
1086
+ const error = await starterMessageResponse.text();
1087
+ s.stop('Failed to create message');
1088
+ throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`);
1089
+ }
1090
+ starterMessage = (await starterMessageResponse.json());
1045
1091
  }
1046
- const starterMessage = (await starterMessageResponse.json());
1047
1092
  s.message('Creating thread...');
1048
1093
  // Create thread from the message
1049
1094
  const threadName = name || (prompt.length > 80 ? prompt.slice(0, 77) + '...' : prompt);
@@ -16,6 +16,9 @@ const sessionLogger = createLogger('SESSION');
16
16
  const voiceLogger = createLogger('VOICE');
17
17
  const discordLogger = createLogger('DISCORD');
18
18
  export const abortControllers = new Map();
19
+ // Track multiple pending permissions per thread (keyed by permission ID)
20
+ // OpenCode handles blocking/sequencing - we just need to track all pending permissions
21
+ // to avoid duplicates and properly clean up on auto-reject
19
22
  export const pendingPermissions = new Map();
20
23
  // Queue of messages waiting to be sent after current response finishes
21
24
  // Key is threadId, value is array of queued messages
@@ -148,26 +151,32 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
148
151
  voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`);
149
152
  existingController.abort(new Error('New request started'));
150
153
  }
151
- const pendingPerm = pendingPermissions.get(thread.id);
152
- if (pendingPerm) {
153
- try {
154
- sessionLogger.log(`[PERMISSION] Auto-rejecting pending permission ${pendingPerm.permission.id} due to new message`);
155
- const clientV2 = getOpencodeClientV2(directory);
156
- if (clientV2) {
157
- await clientV2.permission.reply({
158
- requestID: pendingPerm.permission.id,
159
- reply: 'reject',
160
- });
154
+ // Auto-reject ALL pending permissions for this thread
155
+ const threadPermissions = pendingPermissions.get(thread.id);
156
+ if (threadPermissions && threadPermissions.size > 0) {
157
+ const clientV2 = getOpencodeClientV2(directory);
158
+ let rejectedCount = 0;
159
+ for (const [permId, pendingPerm] of threadPermissions) {
160
+ try {
161
+ sessionLogger.log(`[PERMISSION] Auto-rejecting permission ${permId} due to new message`);
162
+ if (clientV2) {
163
+ await clientV2.permission.reply({
164
+ requestID: permId,
165
+ reply: 'reject',
166
+ });
167
+ }
168
+ cleanupPermissionContext(pendingPerm.contextHash);
169
+ rejectedCount++;
170
+ }
171
+ catch (e) {
172
+ sessionLogger.log(`[PERMISSION] Failed to auto-reject permission ${permId}:`, e);
173
+ cleanupPermissionContext(pendingPerm.contextHash);
161
174
  }
162
- // Clean up both the pending permission and its dropdown context
163
- cleanupPermissionContext(pendingPerm.contextHash);
164
- pendingPermissions.delete(thread.id);
165
- await sendThreadMessage(thread, `⚠️ Previous permission request auto-rejected due to new message`);
166
175
  }
167
- catch (e) {
168
- sessionLogger.log(`[PERMISSION] Failed to auto-reject permission:`, e);
169
- cleanupPermissionContext(pendingPerm.contextHash);
170
- pendingPermissions.delete(thread.id);
176
+ pendingPermissions.delete(thread.id);
177
+ if (rejectedCount > 0) {
178
+ const plural = rejectedCount > 1 ? 's' : '';
179
+ await sendThreadMessage(thread, `⚠️ ${rejectedCount} pending permission request${plural} auto-rejected due to new message`);
171
180
  }
172
181
  }
173
182
  // Cancel any pending question tool if user sends a new message (silently, no thread message)
@@ -382,7 +391,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
382
391
  if (part.type === 'step-start') {
383
392
  // Don't start typing if user needs to respond to a question or permission
384
393
  const hasPendingQuestion = [...pendingQuestionContexts.values()].some((ctx) => ctx.thread.id === thread.id);
385
- const hasPendingPermission = pendingPermissions.has(thread.id);
394
+ const hasPendingPermission = (pendingPermissions.get(thread.id)?.size ?? 0) > 0;
386
395
  if (!hasPendingQuestion && !hasPendingPermission) {
387
396
  stopTyping = startTyping();
388
397
  }
@@ -451,7 +460,7 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
451
460
  return;
452
461
  // Don't restart typing if user needs to respond to a question or permission
453
462
  const hasPendingQuestion = [...pendingQuestionContexts.values()].some((ctx) => ctx.thread.id === thread.id);
454
- const hasPendingPermission = pendingPermissions.has(thread.id);
463
+ const hasPendingPermission = (pendingPermissions.get(thread.id)?.size ?? 0) > 0;
455
464
  if (hasPendingQuestion || hasPendingPermission)
456
465
  return;
457
466
  stopTyping = startTyping();
@@ -487,6 +496,12 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
487
496
  voiceLogger.log(`[PERMISSION IGNORED] Permission for different session (expected: ${session.id}, got: ${permission.sessionID})`);
488
497
  continue;
489
498
  }
499
+ // Skip if this exact permission ID is already pending (dedupe)
500
+ const threadPermissions = pendingPermissions.get(thread.id);
501
+ if (threadPermissions?.has(permission.id)) {
502
+ sessionLogger.log(`[PERMISSION] Skipping duplicate permission ${permission.id} (already pending)`);
503
+ continue;
504
+ }
490
505
  sessionLogger.log(`Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}`);
491
506
  // Stop typing - user needs to respond now, not the bot
492
507
  if (stopTyping) {
@@ -499,7 +514,11 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
499
514
  permission,
500
515
  directory,
501
516
  });
502
- pendingPermissions.set(thread.id, {
517
+ // Track permission in nested map (threadId -> permissionId -> data)
518
+ if (!pendingPermissions.has(thread.id)) {
519
+ pendingPermissions.set(thread.id, new Map());
520
+ }
521
+ pendingPermissions.get(thread.id).set(permission.id, {
503
522
  permission,
504
523
  messageId,
505
524
  directory,
@@ -512,10 +531,18 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
512
531
  continue;
513
532
  }
514
533
  sessionLogger.log(`Permission ${requestID} replied with: ${reply}`);
515
- const pending = pendingPermissions.get(thread.id);
516
- if (pending && pending.permission.id === requestID) {
517
- cleanupPermissionContext(pending.contextHash);
518
- pendingPermissions.delete(thread.id);
534
+ // Clean up the specific permission from nested map
535
+ const threadPermissions = pendingPermissions.get(thread.id);
536
+ if (threadPermissions) {
537
+ const pending = threadPermissions.get(requestID);
538
+ if (pending) {
539
+ cleanupPermissionContext(pending.contextHash);
540
+ threadPermissions.delete(requestID);
541
+ // Remove thread entry if no more pending permissions
542
+ if (threadPermissions.size === 0) {
543
+ pendingPermissions.delete(thread.id);
544
+ }
545
+ }
519
546
  }
520
547
  }
521
548
  else if (event.type === 'question.asked') {
@@ -35,6 +35,21 @@ Use --notify-only to create a notification thread without starting an AI session
35
35
  npx -y kimaki send --channel ${channelId} --prompt "User cancelled subscription" --notify-only
36
36
 
37
37
  This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
38
+
39
+ ### Session handoff
40
+
41
+ When you are approaching the **context window limit** or the user explicitly asks to **handoff to a new thread**, use the \`kimaki send\` command to start a fresh session with context:
42
+
43
+ \`\`\`bash
44
+ npx -y kimaki send --channel ${channelId} --prompt "Continuing from previous session: <summary of current task and state>"
45
+ \`\`\`
46
+
47
+ The command automatically handles long prompts (over 2000 chars) by sending them as file attachments.
48
+
49
+ Use this for handoff when:
50
+ - User asks to "handoff", "continue in new thread", or "start fresh session"
51
+ - You detect you're running low on context window space
52
+ - A complex task would benefit from a clean slate with summarized context
38
53
  `
39
54
  : ''}
40
55
  ## showing diffs
@@ -43,17 +58,17 @@ IMPORTANT: After editing any files, you MUST execute the critique command using
43
58
 
44
59
  Execute this after making changes:
45
60
 
46
- bunx critique web --title "Add user authentication flow"
61
+ bunx critique --web "Add user authentication flow"
47
62
 
48
63
  If there are other unrelated changes in the working directory, filter to only show the files you edited:
49
64
 
50
- bunx critique web --title "Fix database connection retry" -- path/to/file1.ts path/to/file2.ts
65
+ bunx critique --web "Fix database connection retry" --filter "path/to/file1.ts" --filter "path/to/file2.ts"
51
66
 
52
67
  You can also show latest commit changes using:
53
68
 
54
- bunx critique web --title "Refactor API endpoints" HEAD
69
+ bunx critique HEAD --web "Refactor API endpoints"
55
70
 
56
- bunx critique web --title "Update dependencies" HEAD~1 to get the one before last
71
+ bunx critique HEAD~1 --web "Update dependencies"
57
72
 
58
73
  Do this in case you committed the changes yourself (only if the user asks so, never commit otherwise).
59
74
 
@@ -375,6 +375,7 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
375
375
  EmptyTranscriptionError: (e) => e.message,
376
376
  NoResponseContentError: (e) => e.message,
377
377
  NoToolResponseError: (e) => e.message,
378
+ Error: (e) => e.message,
378
379
  });
379
380
  voiceLogger.error(`Transcription failed:`, transcription);
380
381
  await sendThreadMessage(thread, `⚠️ Transcription failed: ${errMsg}`);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.41",
5
+ "version": "0.4.43",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
@@ -41,7 +41,7 @@
41
41
  "string-dedent": "^3.0.2",
42
42
  "undici": "^7.16.0",
43
43
  "zod": "^4.2.1",
44
- "errore": "^0.7.1"
44
+ "errore": "^0.9.0"
45
45
  },
46
46
  "optionalDependencies": {
47
47
  "@discordjs/opus": "^0.10.0",
package/src/cli.ts CHANGED
@@ -1381,28 +1381,83 @@ cli
1381
1381
 
1382
1382
  s.message('Creating starter message...')
1383
1383
 
1384
- // Create starter message with just the prompt (no prefix)
1385
- const starterMessageResponse = await fetch(
1386
- `https://discord.com/api/v10/channels/${channelId}/messages`,
1387
- {
1388
- method: 'POST',
1389
- headers: {
1390
- Authorization: `Bot ${botToken}`,
1391
- 'Content-Type': 'application/json',
1384
+ // Discord has a 2000 character limit for messages.
1385
+ // If prompt exceeds this, send it as a file attachment instead.
1386
+ const DISCORD_MAX_LENGTH = 2000
1387
+ let starterMessage: { id: string }
1388
+
1389
+ if (prompt.length > DISCORD_MAX_LENGTH) {
1390
+ // Send as file attachment with a short summary
1391
+ const preview = prompt.slice(0, 100).replace(/\n/g, ' ')
1392
+ const summaryContent = `📄 **Prompt attached as file** (${prompt.length} chars)\n\n> ${preview}...`
1393
+
1394
+ // Write prompt to a temp file
1395
+ const tmpDir = path.join(process.cwd(), 'tmp')
1396
+ if (!fs.existsSync(tmpDir)) {
1397
+ fs.mkdirSync(tmpDir, { recursive: true })
1398
+ }
1399
+ const tmpFile = path.join(tmpDir, `prompt-${Date.now()}.md`)
1400
+ fs.writeFileSync(tmpFile, prompt)
1401
+
1402
+ try {
1403
+ // Create message with file attachment
1404
+ const formData = new FormData()
1405
+ formData.append(
1406
+ 'payload_json',
1407
+ JSON.stringify({
1408
+ content: summaryContent,
1409
+ attachments: [{ id: 0, filename: 'prompt.md' }],
1410
+ }),
1411
+ )
1412
+ const buffer = fs.readFileSync(tmpFile)
1413
+ formData.append('files[0]', new Blob([buffer], { type: 'text/markdown' }), 'prompt.md')
1414
+
1415
+ const starterMessageResponse = await fetch(
1416
+ `https://discord.com/api/v10/channels/${channelId}/messages`,
1417
+ {
1418
+ method: 'POST',
1419
+ headers: {
1420
+ Authorization: `Bot ${botToken}`,
1421
+ },
1422
+ body: formData,
1423
+ },
1424
+ )
1425
+
1426
+ if (!starterMessageResponse.ok) {
1427
+ const error = await starterMessageResponse.text()
1428
+ s.stop('Failed to create message')
1429
+ throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`)
1430
+ }
1431
+
1432
+ starterMessage = (await starterMessageResponse.json()) as { id: string }
1433
+ } finally {
1434
+ // Clean up temp file
1435
+ fs.unlinkSync(tmpFile)
1436
+ }
1437
+ } else {
1438
+ // Normal case: send prompt inline
1439
+ const starterMessageResponse = await fetch(
1440
+ `https://discord.com/api/v10/channels/${channelId}/messages`,
1441
+ {
1442
+ method: 'POST',
1443
+ headers: {
1444
+ Authorization: `Bot ${botToken}`,
1445
+ 'Content-Type': 'application/json',
1446
+ },
1447
+ body: JSON.stringify({
1448
+ content: prompt,
1449
+ }),
1392
1450
  },
1393
- body: JSON.stringify({
1394
- content: prompt,
1395
- }),
1396
- },
1397
- )
1451
+ )
1398
1452
 
1399
- if (!starterMessageResponse.ok) {
1400
- const error = await starterMessageResponse.text()
1401
- s.stop('Failed to create message')
1402
- throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`)
1403
- }
1453
+ if (!starterMessageResponse.ok) {
1454
+ const error = await starterMessageResponse.text()
1455
+ s.stop('Failed to create message')
1456
+ throw new Error(`Discord API error: ${starterMessageResponse.status} - ${error}`)
1457
+ }
1404
1458
 
1405
- const starterMessage = (await starterMessageResponse.json()) as { id: string }
1459
+ starterMessage = (await starterMessageResponse.json()) as { id: string }
1460
+ }
1406
1461
 
1407
1462
  s.message('Creating thread...')
1408
1463
 
@@ -38,9 +38,12 @@ const discordLogger = createLogger('DISCORD')
38
38
 
39
39
  export const abortControllers = new Map<string, AbortController>()
40
40
 
41
+ // Track multiple pending permissions per thread (keyed by permission ID)
42
+ // OpenCode handles blocking/sequencing - we just need to track all pending permissions
43
+ // to avoid duplicates and properly clean up on auto-reject
41
44
  export const pendingPermissions = new Map<
42
- string,
43
- { permission: PermissionRequest; messageId: string; directory: string; contextHash: string }
45
+ string, // threadId
46
+ Map<string, { permission: PermissionRequest; messageId: string; directory: string; contextHash: string }> // permissionId -> data
44
47
  >()
45
48
 
46
49
  export type QueuedMessage = {
@@ -246,30 +249,34 @@ export async function handleOpencodeSession({
246
249
  existingController.abort(new Error('New request started'))
247
250
  }
248
251
 
249
- const pendingPerm = pendingPermissions.get(thread.id)
250
- if (pendingPerm) {
251
- try {
252
- sessionLogger.log(
253
- `[PERMISSION] Auto-rejecting pending permission ${pendingPerm.permission.id} due to new message`,
254
- )
255
- const clientV2 = getOpencodeClientV2(directory)
256
- if (clientV2) {
257
- await clientV2.permission.reply({
258
- requestID: pendingPerm.permission.id,
259
- reply: 'reject',
260
- })
252
+ // Auto-reject ALL pending permissions for this thread
253
+ const threadPermissions = pendingPermissions.get(thread.id)
254
+ if (threadPermissions && threadPermissions.size > 0) {
255
+ const clientV2 = getOpencodeClientV2(directory)
256
+ let rejectedCount = 0
257
+ for (const [permId, pendingPerm] of threadPermissions) {
258
+ try {
259
+ sessionLogger.log(`[PERMISSION] Auto-rejecting permission ${permId} due to new message`)
260
+ if (clientV2) {
261
+ await clientV2.permission.reply({
262
+ requestID: permId,
263
+ reply: 'reject',
264
+ })
265
+ }
266
+ cleanupPermissionContext(pendingPerm.contextHash)
267
+ rejectedCount++
268
+ } catch (e) {
269
+ sessionLogger.log(`[PERMISSION] Failed to auto-reject permission ${permId}:`, e)
270
+ cleanupPermissionContext(pendingPerm.contextHash)
261
271
  }
262
- // Clean up both the pending permission and its dropdown context
263
- cleanupPermissionContext(pendingPerm.contextHash)
264
- pendingPermissions.delete(thread.id)
272
+ }
273
+ pendingPermissions.delete(thread.id)
274
+ if (rejectedCount > 0) {
275
+ const plural = rejectedCount > 1 ? 's' : ''
265
276
  await sendThreadMessage(
266
277
  thread,
267
- `⚠️ Previous permission request auto-rejected due to new message`,
278
+ `⚠️ ${rejectedCount} pending permission request${plural} auto-rejected due to new message`,
268
279
  )
269
- } catch (e) {
270
- sessionLogger.log(`[PERMISSION] Failed to auto-reject permission:`, e)
271
- cleanupPermissionContext(pendingPerm.contextHash)
272
- pendingPermissions.delete(thread.id)
273
280
  }
274
281
  }
275
282
 
@@ -536,7 +543,7 @@ export async function handleOpencodeSession({
536
543
  const hasPendingQuestion = [...pendingQuestionContexts.values()].some(
537
544
  (ctx) => ctx.thread.id === thread.id,
538
545
  )
539
- const hasPendingPermission = pendingPermissions.has(thread.id)
546
+ const hasPendingPermission = (pendingPermissions.get(thread.id)?.size ?? 0) > 0
540
547
  if (!hasPendingQuestion && !hasPendingPermission) {
541
548
  stopTyping = startTyping()
542
549
  }
@@ -612,7 +619,7 @@ export async function handleOpencodeSession({
612
619
  const hasPendingQuestion = [...pendingQuestionContexts.values()].some(
613
620
  (ctx) => ctx.thread.id === thread.id,
614
621
  )
615
- const hasPendingPermission = pendingPermissions.has(thread.id)
622
+ const hasPendingPermission = (pendingPermissions.get(thread.id)?.size ?? 0) > 0
616
623
  if (hasPendingQuestion || hasPendingPermission) return
617
624
  stopTyping = startTyping()
618
625
  }, 300)
@@ -650,6 +657,15 @@ export async function handleOpencodeSession({
650
657
  continue
651
658
  }
652
659
 
660
+ // Skip if this exact permission ID is already pending (dedupe)
661
+ const threadPermissions = pendingPermissions.get(thread.id)
662
+ if (threadPermissions?.has(permission.id)) {
663
+ sessionLogger.log(
664
+ `[PERMISSION] Skipping duplicate permission ${permission.id} (already pending)`,
665
+ )
666
+ continue
667
+ }
668
+
653
669
  sessionLogger.log(
654
670
  `Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}`,
655
671
  )
@@ -667,7 +683,11 @@ export async function handleOpencodeSession({
667
683
  directory,
668
684
  })
669
685
 
670
- pendingPermissions.set(thread.id, {
686
+ // Track permission in nested map (threadId -> permissionId -> data)
687
+ if (!pendingPermissions.has(thread.id)) {
688
+ pendingPermissions.set(thread.id, new Map())
689
+ }
690
+ pendingPermissions.get(thread.id)!.set(permission.id, {
671
691
  permission,
672
692
  messageId,
673
693
  directory,
@@ -681,10 +701,18 @@ export async function handleOpencodeSession({
681
701
 
682
702
  sessionLogger.log(`Permission ${requestID} replied with: ${reply}`)
683
703
 
684
- const pending = pendingPermissions.get(thread.id)
685
- if (pending && pending.permission.id === requestID) {
686
- cleanupPermissionContext(pending.contextHash)
687
- pendingPermissions.delete(thread.id)
704
+ // Clean up the specific permission from nested map
705
+ const threadPermissions = pendingPermissions.get(thread.id)
706
+ if (threadPermissions) {
707
+ const pending = threadPermissions.get(requestID)
708
+ if (pending) {
709
+ cleanupPermissionContext(pending.contextHash)
710
+ threadPermissions.delete(requestID)
711
+ // Remove thread entry if no more pending permissions
712
+ if (threadPermissions.size === 0) {
713
+ pendingPermissions.delete(thread.id)
714
+ }
715
+ }
688
716
  }
689
717
  } else if (event.type === 'question.asked') {
690
718
  const questionRequest = event.properties
@@ -43,6 +43,21 @@ Use --notify-only to create a notification thread without starting an AI session
43
43
  npx -y kimaki send --channel ${channelId} --prompt "User cancelled subscription" --notify-only
44
44
 
45
45
  This is useful for automation (cron jobs, GitHub webhooks, n8n, etc.)
46
+
47
+ ### Session handoff
48
+
49
+ When you are approaching the **context window limit** or the user explicitly asks to **handoff to a new thread**, use the \`kimaki send\` command to start a fresh session with context:
50
+
51
+ \`\`\`bash
52
+ npx -y kimaki send --channel ${channelId} --prompt "Continuing from previous session: <summary of current task and state>"
53
+ \`\`\`
54
+
55
+ The command automatically handles long prompts (over 2000 chars) by sending them as file attachments.
56
+
57
+ Use this for handoff when:
58
+ - User asks to "handoff", "continue in new thread", or "start fresh session"
59
+ - You detect you're running low on context window space
60
+ - A complex task would benefit from a clean slate with summarized context
46
61
  `
47
62
  : ''
48
63
  }
@@ -52,17 +67,17 @@ IMPORTANT: After editing any files, you MUST execute the critique command using
52
67
 
53
68
  Execute this after making changes:
54
69
 
55
- bunx critique web --title "Add user authentication flow"
70
+ bunx critique --web "Add user authentication flow"
56
71
 
57
72
  If there are other unrelated changes in the working directory, filter to only show the files you edited:
58
73
 
59
- bunx critique web --title "Fix database connection retry" -- path/to/file1.ts path/to/file2.ts
74
+ bunx critique --web "Fix database connection retry" --filter "path/to/file1.ts" --filter "path/to/file2.ts"
60
75
 
61
76
  You can also show latest commit changes using:
62
77
 
63
- bunx critique web --title "Refactor API endpoints" HEAD
78
+ bunx critique HEAD --web "Refactor API endpoints"
64
79
 
65
- bunx critique web --title "Update dependencies" HEAD~1 to get the one before last
80
+ bunx critique HEAD~1 --web "Update dependencies"
66
81
 
67
82
  Do this in case you committed the changes yourself (only if the user asks so, never commit otherwise).
68
83
 
@@ -506,6 +506,7 @@ export async function processVoiceAttachment({
506
506
  EmptyTranscriptionError: (e) => e.message,
507
507
  NoResponseContentError: (e) => e.message,
508
508
  NoToolResponseError: (e) => e.message,
509
+ Error: (e) => e.message,
509
510
  })
510
511
  voiceLogger.error(`Transcription failed:`, transcription)
511
512
  await sendThreadMessage(thread, `⚠️ Transcription failed: ${errMsg}`)