kimaki 0.4.80 → 0.4.81
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/anthropic-auth-plugin.js +114 -72
- package/dist/cli.js +5 -0
- package/dist/commands/memory-snapshot.js +24 -0
- package/dist/discord-bot.js +11 -4
- package/dist/interaction-handler.js +7 -0
- package/dist/session-handler/thread-session-runtime.js +71 -14
- package/package.json +1 -1
- package/src/anthropic-auth-plugin.ts +121 -70
- package/src/cli.ts +7 -0
- package/src/commands/memory-snapshot.ts +30 -0
- package/src/discord-bot.ts +14 -4
- package/src/interaction-handler.ts +8 -0
- package/src/session-handler/thread-session-runtime.ts +87 -15
- package/skills/lintcn/SKILL.md +0 -749
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts
|
|
16
16
|
*/
|
|
17
17
|
import { generatePKCE } from "@openauthjs/openauth/pkce";
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
18
19
|
import * as fs from "node:fs/promises";
|
|
19
20
|
import { createServer } from "node:http";
|
|
20
21
|
import { homedir } from "node:os";
|
|
@@ -62,37 +63,88 @@ const OPENCODE_TO_CLAUDE_CODE_TOOL_NAME = {
|
|
|
62
63
|
write: "Write",
|
|
63
64
|
};
|
|
64
65
|
// --- HTTP helpers ---
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
// Claude OAuth token exchange can 429 when this runs inside the opencode auth
|
|
67
|
+
// process, even with the same payload that succeeds in a plain Node process.
|
|
68
|
+
// Run these OAuth-only HTTP calls in an isolated Node child to avoid whatever
|
|
69
|
+
// parent-process runtime state is affecting the in-process requests.
|
|
70
|
+
async function requestText(urlString, options) {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const payload = JSON.stringify({
|
|
73
|
+
body: options.body,
|
|
74
|
+
headers: options.headers,
|
|
75
|
+
method: options.method,
|
|
76
|
+
url: urlString,
|
|
77
|
+
});
|
|
78
|
+
const child = spawn("node", ["-e", `
|
|
79
|
+
const input = JSON.parse(process.argv[1]);
|
|
80
|
+
(async () => {
|
|
81
|
+
const response = await fetch(input.url, {
|
|
82
|
+
method: input.method,
|
|
83
|
+
headers: input.headers,
|
|
84
|
+
body: input.body,
|
|
85
|
+
});
|
|
86
|
+
const text = await response.text();
|
|
87
|
+
if (!response.ok) {
|
|
88
|
+
console.error(JSON.stringify({ status: response.status, body: text }));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
process.stdout.write(text);
|
|
92
|
+
})().catch((error) => {
|
|
93
|
+
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
|
96
|
+
`.trim(), payload], {
|
|
97
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
98
|
+
});
|
|
99
|
+
let stdout = "";
|
|
100
|
+
let stderr = "";
|
|
101
|
+
const timeout = setTimeout(() => {
|
|
102
|
+
child.kill();
|
|
103
|
+
reject(new Error(`Request timed out. url=${urlString}`));
|
|
104
|
+
}, 30_000);
|
|
105
|
+
child.stdout.on("data", (chunk) => {
|
|
106
|
+
stdout += String(chunk);
|
|
107
|
+
});
|
|
108
|
+
child.stderr.on("data", (chunk) => {
|
|
109
|
+
stderr += String(chunk);
|
|
110
|
+
});
|
|
111
|
+
child.on("error", (error) => {
|
|
112
|
+
clearTimeout(timeout);
|
|
113
|
+
reject(error);
|
|
114
|
+
});
|
|
115
|
+
child.on("close", (code) => {
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
if (code !== 0) {
|
|
118
|
+
let details = stderr.trim();
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(details);
|
|
121
|
+
if (typeof parsed.status === "number") {
|
|
122
|
+
reject(new Error(`HTTP ${parsed.status} from ${urlString}: ${parsed.body ?? ""}`));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// fall back to raw stderr
|
|
128
|
+
}
|
|
129
|
+
reject(new Error(details || `Node helper exited with code ${code}`));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
resolve(stdout);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
69
135
|
}
|
|
70
136
|
async function postJson(url, body) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const delayMs = retryAfter
|
|
83
|
-
? Math.min(Number(retryAfter) * 1000, 60_000)
|
|
84
|
-
: BASE_DELAY_MS * 2 ** attempt;
|
|
85
|
-
console.warn(`[anthropic-auth] 429 from ${url}, retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
86
|
-
await sleep(delayMs);
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (!response.ok) {
|
|
90
|
-
const text = await response.text().catch(() => "");
|
|
91
|
-
throw new Error(`HTTP ${response.status} from ${url}: ${text}`);
|
|
92
|
-
}
|
|
93
|
-
return response.json();
|
|
94
|
-
}
|
|
95
|
-
throw new Error(`Exhausted retries for ${url}`);
|
|
137
|
+
const requestBody = JSON.stringify(body);
|
|
138
|
+
const responseText = await requestText(url, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
Accept: "application/json",
|
|
142
|
+
"Content-Length": String(Buffer.byteLength(requestBody)),
|
|
143
|
+
"Content-Type": "application/json",
|
|
144
|
+
},
|
|
145
|
+
body: requestBody,
|
|
146
|
+
});
|
|
147
|
+
return JSON.parse(responseText);
|
|
96
148
|
}
|
|
97
149
|
// --- File lock for token refresh ---
|
|
98
150
|
let pendingRefresh;
|
|
@@ -163,33 +215,16 @@ async function refreshAnthropicToken(refreshToken) {
|
|
|
163
215
|
};
|
|
164
216
|
}
|
|
165
217
|
async function createApiKey(accessToken) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (response.status === 429 && attempt < MAX_RETRIES) {
|
|
177
|
-
const retryAfter = response.headers.get("retry-after");
|
|
178
|
-
const delayMs = retryAfter
|
|
179
|
-
? Math.min(Number(retryAfter) * 1000, 60_000)
|
|
180
|
-
: BASE_DELAY_MS * 2 ** attempt;
|
|
181
|
-
console.warn(`[anthropic-auth] 429 creating API key, retrying in ${delayMs}ms (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
182
|
-
await sleep(delayMs);
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
if (!response.ok) {
|
|
186
|
-
const text = await response.text().catch(() => "");
|
|
187
|
-
throw new Error(`HTTP ${response.status} creating API key: ${text}`);
|
|
188
|
-
}
|
|
189
|
-
const json = (await response.json());
|
|
190
|
-
return { type: "success", key: json.raw_key };
|
|
191
|
-
}
|
|
192
|
-
throw new Error(`Exhausted retries for ${CREATE_API_KEY_URL}`);
|
|
218
|
+
const responseText = await requestText(CREATE_API_KEY_URL, {
|
|
219
|
+
method: "POST",
|
|
220
|
+
headers: {
|
|
221
|
+
Accept: "application/json",
|
|
222
|
+
authorization: `Bearer ${accessToken}`,
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
const json = JSON.parse(responseText);
|
|
227
|
+
return { type: "success", key: json.raw_key };
|
|
193
228
|
}
|
|
194
229
|
async function startCallbackServer(expectedState) {
|
|
195
230
|
return new Promise((resolve, reject) => {
|
|
@@ -317,6 +352,7 @@ function buildAuthorizeHandler(mode) {
|
|
|
317
352
|
return async () => {
|
|
318
353
|
const auth = await beginAuthorizationFlow();
|
|
319
354
|
const isRemote = Boolean(process.env.KIMAKI);
|
|
355
|
+
let pendingAuthResult;
|
|
320
356
|
const finalize = async (result) => {
|
|
321
357
|
const verifier = auth.verifier;
|
|
322
358
|
const creds = await exchangeAuthorizationCode(result.code, result.state || verifier, verifier, REDIRECT_URI);
|
|
@@ -331,14 +367,17 @@ function buildAuthorizeHandler(mode) {
|
|
|
331
367
|
instructions: "Complete login in your browser on this machine. OpenCode will catch the localhost callback automatically.",
|
|
332
368
|
method: "auto",
|
|
333
369
|
callback: async () => {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
370
|
+
pendingAuthResult ??= (async () => {
|
|
371
|
+
try {
|
|
372
|
+
const result = await waitForCallback(auth.callbackServer);
|
|
373
|
+
return await finalize(result);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
console.error(`[anthropic-auth] ${error}`);
|
|
377
|
+
return { type: "failed" };
|
|
378
|
+
}
|
|
379
|
+
})();
|
|
380
|
+
return pendingAuthResult;
|
|
342
381
|
},
|
|
343
382
|
};
|
|
344
383
|
}
|
|
@@ -347,14 +386,17 @@ function buildAuthorizeHandler(mode) {
|
|
|
347
386
|
instructions: "Complete login in your browser, then paste the final redirect URL from the address bar here. Pasting just the authorization code also works.",
|
|
348
387
|
method: "code",
|
|
349
388
|
callback: async (input) => {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
389
|
+
pendingAuthResult ??= (async () => {
|
|
390
|
+
try {
|
|
391
|
+
const result = await waitForCallback(auth.callbackServer, input);
|
|
392
|
+
return await finalize(result);
|
|
393
|
+
}
|
|
394
|
+
catch (error) {
|
|
395
|
+
console.error(`[anthropic-auth] ${error}`);
|
|
396
|
+
return { type: "failed" };
|
|
397
|
+
}
|
|
398
|
+
})();
|
|
399
|
+
return pendingAuthResult;
|
|
358
400
|
},
|
|
359
401
|
};
|
|
360
402
|
};
|
package/dist/cli.js
CHANGED
|
@@ -768,6 +768,11 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
768
768
|
.setDescription(truncateCommandDescription('Show current session ID and opencode attach command for this thread'))
|
|
769
769
|
.setDMPermission(false)
|
|
770
770
|
.toJSON(),
|
|
771
|
+
new SlashCommandBuilder()
|
|
772
|
+
.setName('memory-snapshot')
|
|
773
|
+
.setDescription(truncateCommandDescription('Write a V8 heap snapshot to disk for memory debugging'))
|
|
774
|
+
.setDMPermission(false)
|
|
775
|
+
.toJSON(),
|
|
771
776
|
new SlashCommandBuilder()
|
|
772
777
|
.setName('upgrade-and-restart')
|
|
773
778
|
.setDescription(truncateCommandDescription('Upgrade kimaki to the latest version and restart the bot'))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// /memory-snapshot command - Write a V8 heap snapshot and show the file path.
|
|
2
|
+
// Reuses writeHeapSnapshot() from heap-monitor.ts which writes gzip-compressed
|
|
3
|
+
// .heapsnapshot.gz files to ~/.kimaki/heap-snapshots/.
|
|
4
|
+
import { MessageFlags } from 'discord.js';
|
|
5
|
+
import { writeHeapSnapshot } from '../heap-monitor.js';
|
|
6
|
+
import { SILENT_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
7
|
+
import { createLogger, LogPrefix } from '../logger.js';
|
|
8
|
+
const logger = createLogger(LogPrefix.HEAP);
|
|
9
|
+
export async function handleMemorySnapshotCommand({ command, }) {
|
|
10
|
+
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
11
|
+
try {
|
|
12
|
+
const filepath = await writeHeapSnapshot();
|
|
13
|
+
await command.editReply({
|
|
14
|
+
content: `Heap snapshot written:\n\`${filepath}\``,
|
|
15
|
+
});
|
|
16
|
+
logger.log(`Memory snapshot requested via /memory-snapshot: ${filepath}`);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
20
|
+
await command.editReply({
|
|
21
|
+
content: `Failed to write heap snapshot: ${msg}`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/discord-bot.js
CHANGED
|
@@ -336,12 +336,19 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
336
336
|
if (isThread) {
|
|
337
337
|
const thread = channel;
|
|
338
338
|
discordLogger.log(`Message in thread ${thread.name} (${thread.id})`);
|
|
339
|
-
// Only respond in threads kimaki knows about (has a session row in DB)
|
|
340
|
-
//
|
|
341
|
-
//
|
|
339
|
+
// Only respond in threads kimaki knows about (has a session row in DB),
|
|
340
|
+
// where the bot is explicitly @mentioned, or where the bot created the
|
|
341
|
+
// thread itself (e.g. /new-worktree, /fork, kimaki send). This prevents
|
|
342
|
+
// the bot from hijacking user-created threads in project channels while
|
|
343
|
+
// still responding to bot-created threads that may not yet have a session
|
|
344
|
+
// row with a non-empty session_id (createPendingWorktree sets ''). (GitHub #84)
|
|
342
345
|
const hasExistingSession = await getThreadSession(thread.id);
|
|
343
346
|
const botMentioned = discordClient.user && message.mentions.has(discordClient.user.id);
|
|
344
|
-
|
|
347
|
+
const botCreatedThread = discordClient.user && thread.ownerId === discordClient.user.id;
|
|
348
|
+
if (!hasExistingSession &&
|
|
349
|
+
!botMentioned &&
|
|
350
|
+
!isCliInjectedPrompt &&
|
|
351
|
+
!botCreatedThread) {
|
|
345
352
|
discordLogger.log(`Ignoring thread ${thread.id}: no existing session and bot not mentioned`);
|
|
346
353
|
return;
|
|
347
354
|
}
|
|
@@ -36,6 +36,7 @@ import { handleRestartOpencodeServerCommand } from './commands/restart-opencode-
|
|
|
36
36
|
import { handleRunCommand } from './commands/run-command.js';
|
|
37
37
|
import { handleContextUsageCommand } from './commands/context-usage.js';
|
|
38
38
|
import { handleSessionIdCommand } from './commands/session-id.js';
|
|
39
|
+
import { handleMemorySnapshotCommand } from './commands/memory-snapshot.js';
|
|
39
40
|
import { handleUpgradeAndRestartCommand } from './commands/upgrade.js';
|
|
40
41
|
import { handleMcpCommand, handleMcpSelectMenu } from './commands/mcp.js';
|
|
41
42
|
import { handleScreenshareCommand, handleScreenshareStopCommand, } from './commands/screenshare.js';
|
|
@@ -204,6 +205,12 @@ export function registerInteractionHandler({ discordClient, appId, }) {
|
|
|
204
205
|
case 'session-id':
|
|
205
206
|
await handleSessionIdCommand({ command: interaction, appId });
|
|
206
207
|
return;
|
|
208
|
+
case 'memory-snapshot':
|
|
209
|
+
await handleMemorySnapshotCommand({
|
|
210
|
+
command: interaction,
|
|
211
|
+
appId,
|
|
212
|
+
});
|
|
213
|
+
return;
|
|
207
214
|
case 'upgrade-and-restart':
|
|
208
215
|
await handleUpgradeAndRestartCommand({
|
|
209
216
|
command: interaction,
|
|
@@ -574,9 +574,53 @@ export class ThreadSessionRuntime {
|
|
|
574
574
|
}
|
|
575
575
|
return `${text.slice(0, ThreadSessionRuntime.EVENT_BUFFER_TEXT_MAX_CHARS)}…`;
|
|
576
576
|
}
|
|
577
|
+
isDefinedEventBufferValue(value) {
|
|
578
|
+
return value !== undefined;
|
|
579
|
+
}
|
|
580
|
+
pruneLargeStringsForEventBuffer(value, seen) {
|
|
581
|
+
if (typeof value !== 'object' || value === null) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (seen.has(value)) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
seen.add(value);
|
|
588
|
+
if (Array.isArray(value)) {
|
|
589
|
+
const compactedItems = value
|
|
590
|
+
.map((item) => {
|
|
591
|
+
if (typeof item === 'string') {
|
|
592
|
+
if (item.length > ThreadSessionRuntime.EVENT_BUFFER_TEXT_MAX_CHARS) {
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
return item;
|
|
596
|
+
}
|
|
597
|
+
this.pruneLargeStringsForEventBuffer(item, seen);
|
|
598
|
+
return item;
|
|
599
|
+
})
|
|
600
|
+
.filter((item) => {
|
|
601
|
+
return this.isDefinedEventBufferValue(item);
|
|
602
|
+
});
|
|
603
|
+
value.splice(0, value.length, ...compactedItems);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const objectValue = value;
|
|
607
|
+
for (const [key, nestedValue] of Object.entries(objectValue)) {
|
|
608
|
+
if (typeof nestedValue === 'string') {
|
|
609
|
+
if (nestedValue.length > ThreadSessionRuntime.EVENT_BUFFER_TEXT_MAX_CHARS) {
|
|
610
|
+
delete objectValue[key];
|
|
611
|
+
}
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
this.pruneLargeStringsForEventBuffer(nestedValue, seen);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
finalizeCompactedEventForEventBuffer(event) {
|
|
618
|
+
this.pruneLargeStringsForEventBuffer(event, new WeakSet());
|
|
619
|
+
return event;
|
|
620
|
+
}
|
|
577
621
|
compactEventForEventBuffer(event) {
|
|
578
|
-
if (event.type
|
|
579
|
-
return
|
|
622
|
+
if (event.type === 'session.diff') {
|
|
623
|
+
return undefined;
|
|
580
624
|
}
|
|
581
625
|
const compacted = structuredClone(event);
|
|
582
626
|
if (compacted.type === 'message.updated') {
|
|
@@ -590,27 +634,30 @@ export class ThreadSessionRuntime {
|
|
|
590
634
|
delete info.summary;
|
|
591
635
|
delete info.tools;
|
|
592
636
|
delete info.parts;
|
|
593
|
-
return compacted;
|
|
637
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
638
|
+
}
|
|
639
|
+
if (compacted.type !== 'message.part.updated') {
|
|
640
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
594
641
|
}
|
|
595
642
|
const part = compacted.properties.part;
|
|
596
643
|
if (part.type === 'text') {
|
|
597
644
|
part.text = this.compactTextForEventBuffer(part.text);
|
|
598
|
-
return compacted;
|
|
645
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
599
646
|
}
|
|
600
647
|
if (part.type === 'reasoning') {
|
|
601
648
|
part.text = this.compactTextForEventBuffer(part.text);
|
|
602
|
-
return compacted;
|
|
649
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
603
650
|
}
|
|
604
651
|
if (part.type === 'snapshot') {
|
|
605
652
|
part.snapshot = this.compactTextForEventBuffer(part.snapshot);
|
|
606
|
-
return compacted;
|
|
653
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
607
654
|
}
|
|
608
655
|
if (part.type === 'step-start' && part.snapshot) {
|
|
609
656
|
part.snapshot = this.compactTextForEventBuffer(part.snapshot);
|
|
610
|
-
return compacted;
|
|
657
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
611
658
|
}
|
|
612
659
|
if (part.type !== 'tool') {
|
|
613
|
-
return compacted;
|
|
660
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
614
661
|
}
|
|
615
662
|
const state = part.state;
|
|
616
663
|
// Preserve subagent_type for task tools so derivation can build labels
|
|
@@ -622,28 +669,32 @@ export class ThreadSessionRuntime {
|
|
|
622
669
|
}
|
|
623
670
|
if (state.status === 'pending') {
|
|
624
671
|
state.raw = this.compactTextForEventBuffer(state.raw);
|
|
625
|
-
return compacted;
|
|
672
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
626
673
|
}
|
|
627
674
|
if (state.status === 'running') {
|
|
628
|
-
return compacted;
|
|
675
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
629
676
|
}
|
|
630
677
|
if (state.status === 'completed') {
|
|
631
678
|
state.output = this.compactTextForEventBuffer(state.output);
|
|
632
679
|
delete state.attachments;
|
|
633
|
-
return compacted;
|
|
680
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
634
681
|
}
|
|
635
682
|
if (state.status === 'error') {
|
|
636
683
|
state.error = this.compactTextForEventBuffer(state.error);
|
|
637
|
-
return compacted;
|
|
684
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
638
685
|
}
|
|
639
|
-
return compacted;
|
|
686
|
+
return this.finalizeCompactedEventForEventBuffer(compacted);
|
|
640
687
|
}
|
|
641
688
|
appendEventToBuffer(event) {
|
|
689
|
+
const compactedEvent = this.compactEventForEventBuffer(event);
|
|
690
|
+
if (!compactedEvent) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
642
693
|
const timestamp = Date.now();
|
|
643
694
|
const eventIndex = this.nextEventIndex;
|
|
644
695
|
this.nextEventIndex += 1;
|
|
645
696
|
this.eventBuffer.push({
|
|
646
|
-
event:
|
|
697
|
+
event: compactedEvent,
|
|
647
698
|
timestamp,
|
|
648
699
|
eventIndex,
|
|
649
700
|
});
|
|
@@ -791,6 +842,12 @@ export class ThreadSessionRuntime {
|
|
|
791
842
|
// Global events (tui.toast.show) bypass the guard.
|
|
792
843
|
// Subtask sessions also bypass — they're tracked in subtaskSessions.
|
|
793
844
|
async handleEvent(event) {
|
|
845
|
+
// session.diff can carry repeated full-file before/after snapshots and is
|
|
846
|
+
// not used by event-derived runtime state, queueing, typing, or UI routing.
|
|
847
|
+
// Drop it at ingress so large diff payloads never hit memory buffers.
|
|
848
|
+
if (event.type === 'session.diff') {
|
|
849
|
+
return;
|
|
850
|
+
}
|
|
794
851
|
// Skip message.part.delta from the event buffer — no derivation function
|
|
795
852
|
// (isSessionBusy, doesLatestUserTurnHaveNaturalCompletion, waitForEvent,
|
|
796
853
|
// etc.) uses them. During long streaming responses they flood the 1000-slot
|