kimiflare 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -35,6 +35,10 @@ async function loadConfig() {
35
35
  const envEffort = readReasoningEffortEnv();
36
36
  const envTheme = process.env.KIMI_THEME;
37
37
  const envCoauthor = readCoauthorEnv();
38
+ const envCacheStable = process.env.KIMIFLARE_CACHE_STABLE_PROMPTS;
39
+ const cacheStablePrompts = envCacheStable === "0" || envCacheStable === "false" ? false : true;
40
+ const envCompiled = process.env.KIMIFLARE_COMPILED_CONTEXT;
41
+ const compiledContext = envCompiled === "1" || envCompiled === "true" ? true : false;
38
42
  if (envAccount && envToken) {
39
43
  return {
40
44
  accountId: envAccount,
@@ -44,7 +48,9 @@ async function loadConfig() {
44
48
  reasoningEffort: envEffort,
45
49
  coauthor: envCoauthor?.enabled ?? true,
46
50
  coauthorName: envCoauthor?.name,
47
- coauthorEmail: envCoauthor?.email
51
+ coauthorEmail: envCoauthor?.email,
52
+ cacheStablePrompts,
53
+ compiledContext
48
54
  };
49
55
  }
50
56
  try {
@@ -60,7 +66,9 @@ async function loadConfig() {
60
66
  coauthor: envCoauthor?.enabled ?? parsed.coauthor ?? true,
61
67
  coauthorName: envCoauthor?.name ?? parsed.coauthorName,
62
68
  coauthorEmail: envCoauthor?.email ?? parsed.coauthorEmail,
63
- mcpServers: parsed.mcpServers
69
+ mcpServers: parsed.mcpServers,
70
+ cacheStablePrompts: parsed.cacheStablePrompts ?? cacheStablePrompts,
71
+ compiledContext: parsed.compiledContext ?? compiledContext
64
72
  };
65
73
  }
66
74
  } catch {
@@ -153,6 +161,20 @@ function jsonReplacer(_key, value) {
153
161
  }
154
162
  return value;
155
163
  }
164
+ function stableStringify(value, replacer, space) {
165
+ function sortKeys(obj) {
166
+ if (obj === null || typeof obj !== "object") return obj;
167
+ if (Array.isArray(obj)) return obj.map(sortKeys);
168
+ const sorted2 = {};
169
+ const keys = Object.keys(obj).sort();
170
+ for (const k of keys) {
171
+ sorted2[k] = sortKeys(obj[k]);
172
+ }
173
+ return sorted2;
174
+ }
175
+ const sorted = sortKeys(value);
176
+ return JSON.stringify(sorted, replacer, space);
177
+ }
156
178
  var init_messages = __esm({
157
179
  "src/agent/messages.ts"() {
158
180
  "use strict";
@@ -185,13 +207,17 @@ async function* runKimi(opts2) {
185
207
  for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
186
208
  let res;
187
209
  try {
210
+ const headers = {
211
+ Authorization: `Bearer ${opts2.apiToken}`,
212
+ "Content-Type": "application/json"
213
+ };
214
+ if (opts2.sessionId) {
215
+ headers["X-Session-ID"] = opts2.sessionId;
216
+ }
188
217
  res = await fetch(url, {
189
218
  method: "POST",
190
- headers: {
191
- Authorization: `Bearer ${opts2.apiToken}`,
192
- "Content-Type": "application/json"
193
- },
194
- body: JSON.stringify(body, jsonReplacer),
219
+ headers,
220
+ body: stableStringify(body, jsonReplacer),
195
221
  signal: opts2.signal
196
222
  });
197
223
  } catch (fetchErr) {
@@ -384,16 +410,107 @@ var init_registry = __esm({
384
410
  }
385
411
  });
386
412
 
413
+ // src/storage-limits.ts
414
+ import { readdir, stat, unlink } from "fs/promises";
415
+ import { join as join2 } from "path";
416
+ async function listFilesByMtime(dir, pattern = /.*/) {
417
+ let entries;
418
+ try {
419
+ entries = await readdir(dir);
420
+ } catch {
421
+ return [];
422
+ }
423
+ const files = [];
424
+ for (const name of entries) {
425
+ if (!pattern.test(name)) continue;
426
+ const p = join2(dir, name);
427
+ try {
428
+ const s = await stat(p);
429
+ if (s.isFile()) files.push({ path: p, mtime: s.mtime });
430
+ } catch {
431
+ }
432
+ }
433
+ files.sort((a, b) => b.mtime < a.mtime ? -1 : 1);
434
+ return files;
435
+ }
436
+ async function pruneFiles(files, maxAgeDays, maxCount) {
437
+ const cutoff = new Date(Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3);
438
+ let removed = 0;
439
+ for (const f of files) {
440
+ if (f.mtime < cutoff) {
441
+ try {
442
+ await unlink(f.path);
443
+ removed++;
444
+ } catch {
445
+ }
446
+ }
447
+ }
448
+ const remaining = files.filter((f) => {
449
+ return f.mtime >= cutoff;
450
+ });
451
+ if (remaining.length > maxCount) {
452
+ const toDelete = remaining.slice(maxCount);
453
+ for (const f of toDelete) {
454
+ try {
455
+ await unlink(f.path);
456
+ removed++;
457
+ } catch {
458
+ }
459
+ }
460
+ }
461
+ return removed;
462
+ }
463
+ async function rotateJsonl(path, maxBytes, rotations) {
464
+ const { rename } = await import("fs/promises");
465
+ let s;
466
+ try {
467
+ s = await stat(path);
468
+ } catch {
469
+ return;
470
+ }
471
+ if (s.size <= maxBytes) return;
472
+ for (let i = rotations - 1; i >= 1; i--) {
473
+ const src = i === 1 ? path : `${path}.${i - 1}`;
474
+ const dst = `${path}.${i}`;
475
+ try {
476
+ await rename(src, dst);
477
+ } catch {
478
+ }
479
+ }
480
+ }
481
+ var RETENTION;
482
+ var init_storage_limits = __esm({
483
+ "src/storage-limits.ts"() {
484
+ "use strict";
485
+ RETENTION = {
486
+ /** Session files older than this (days) are pruned. */
487
+ sessionMaxAgeDays: 30,
488
+ /** Max number of session files to keep. */
489
+ sessionMaxCount: 100,
490
+ /** Usage log day entries older than this (days) are pruned. */
491
+ usageDayMaxAgeDays: 90,
492
+ /** Usage log session entries older than this (days) are pruned. */
493
+ usageSessionMaxAgeDays: 30,
494
+ /** Max number of session entries in usage log. */
495
+ usageSessionMaxCount: 200,
496
+ /** Max size of cost-debug JSONL before rotation (bytes). */
497
+ costDebugMaxBytes: 5 * 1024 * 1024,
498
+ /** Number of rotated cost-debug files to keep. */
499
+ costDebugRotations: 2
500
+ };
501
+ }
502
+ });
503
+
387
504
  // src/cost-debug.ts
388
505
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
389
506
  import { homedir as homedir2 } from "os";
390
- import { join as join2 } from "path";
507
+ import { join as join3 } from "path";
391
508
  function debugDir() {
392
- const xdg = process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share");
393
- return join2(xdg, "kimiflare");
509
+ const xdg = process.env.XDG_DATA_HOME || join3(homedir2(), ".local", "share");
510
+ return join3(xdg, "kimiflare");
394
511
  }
395
512
  function debugPath() {
396
- return join2(debugDir(), "cost-debug.jsonl");
513
+ return join3(debugDir(), "cost-debug.jsonl");
397
514
  }
398
515
  function now() {
399
516
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -449,14 +566,79 @@ function buildToolStats(results) {
449
566
  }
450
567
  async function logCostDebug(entry) {
451
568
  await mkdir2(debugDir(), { recursive: true });
569
+ await rotateJsonl(debugPath(), RETENTION.costDebugMaxBytes, RETENTION.costDebugRotations);
452
570
  await appendFile(debugPath(), JSON.stringify(entry) + "\n", "utf8");
453
571
  }
572
+ function serializePrefix(messages) {
573
+ let end = 0;
574
+ while (end < messages.length && messages[end].role === "system") {
575
+ end++;
576
+ }
577
+ return messages.slice(0, end).map((m) => typeof m.content === "string" ? m.content : JSON.stringify(m.content)).join("\n---\n");
578
+ }
579
+ function comparePromptPrefixes(prev, curr) {
580
+ const prevPrefix = prev ? serializePrefix(prev) : "";
581
+ const currPrefix = serializePrefix(curr);
582
+ const totalChars = curr.reduce((sum, m) => {
583
+ if (typeof m.content === "string") return sum + m.content.length;
584
+ if (Array.isArray(m.content)) return sum + m.content.map((p) => p.type === "text" ? p.text.length : 0).reduce((a, b) => a + b, 0);
585
+ return sum;
586
+ }, 0);
587
+ let firstDiffByte = null;
588
+ let changedSegment = null;
589
+ if (prevPrefix !== currPrefix) {
590
+ const minLen = Math.min(prevPrefix.length, currPrefix.length);
591
+ for (let i = 0; i < minLen; i++) {
592
+ if (prevPrefix[i] !== currPrefix[i]) {
593
+ firstDiffByte = i;
594
+ break;
595
+ }
596
+ }
597
+ if (firstDiffByte === null && prevPrefix.length !== currPrefix.length) {
598
+ firstDiffByte = minLen;
599
+ }
600
+ if (curr.length >= 1 && curr[0].role === "system") {
601
+ const staticLen = typeof curr[0].content === "string" ? curr[0].content.length : JSON.stringify(curr[0].content).length;
602
+ if (firstDiffByte !== null && firstDiffByte < staticLen) {
603
+ changedSegment = "static";
604
+ } else if (curr.length >= 2 && curr[1].role === "system") {
605
+ const sessionLen = typeof curr[1].content === "string" ? curr[1].content.length : JSON.stringify(curr[1].content).length;
606
+ if (firstDiffByte !== null && firstDiffByte < staticLen + 5 + sessionLen) {
607
+ changedSegment = "session";
608
+ } else {
609
+ changedSegment = "dynamic";
610
+ }
611
+ } else {
612
+ changedSegment = "dynamic";
613
+ }
614
+ } else {
615
+ changedSegment = "dynamic";
616
+ }
617
+ } else {
618
+ changedSegment = "none";
619
+ }
620
+ const staticPrefixChars = curr.length > 0 && curr[0].role === "system" && typeof curr[0].content === "string" ? curr[0].content.length : 0;
621
+ const sessionPrefixChars = curr.length > 1 && curr[1].role === "system" && typeof curr[1].content === "string" ? curr[1].content.length : 0;
622
+ const dynamicSuffixChars = totalChars - staticPrefixChars - sessionPrefixChars;
623
+ return {
624
+ staticPrefixChars,
625
+ sessionPrefixChars,
626
+ dynamicSuffixChars,
627
+ firstDiffByte,
628
+ changedSegment,
629
+ cacheHitRatio: 0
630
+ // populated by caller with actual usage data
631
+ };
632
+ }
454
633
  async function logTurnDebug(ctx) {
455
634
  const promptSections = analyzePrompt(ctx.messages);
456
635
  const promptTotalChars = promptSections.reduce((sum, s) => sum + s.chars, 0);
457
636
  const toolStats = buildToolStats(ctx.toolResults);
458
637
  const toolTotalRaw = toolStats.reduce((sum, t) => sum + t.rawBytes, 0);
459
638
  const toolTotalReduced = toolStats.reduce((sum, t) => sum + t.reducedBytes, 0);
639
+ const cacheDiagnostics = comparePromptPrefixes(ctx.previousMessages, ctx.messages);
640
+ const cachedTokens = ctx.usage.prompt_tokens_details?.cached_tokens ?? 0;
641
+ cacheDiagnostics.cacheHitRatio = ctx.usage.prompt_tokens > 0 ? cachedTokens / ctx.usage.prompt_tokens : 0;
460
642
  await logCostDebug({
461
643
  v: LOG_VERSION,
462
644
  ts: now(),
@@ -469,13 +651,16 @@ async function logTurnDebug(ctx) {
469
651
  toolStats,
470
652
  toolTotalRawBytes: toolTotalRaw,
471
653
  toolTotalReducedBytes: toolTotalReduced,
472
- toolSavingsPct: toolTotalRaw > 0 ? Math.round((toolTotalRaw - toolTotalReduced) / toolTotalRaw * 100) : 0
654
+ toolSavingsPct: toolTotalRaw > 0 ? Math.round((toolTotalRaw - toolTotalReduced) / toolTotalRaw * 100) : 0,
655
+ cacheDiagnostics,
656
+ compaction: ctx.compaction
473
657
  });
474
658
  }
475
659
  var LOG_VERSION;
476
660
  var init_cost_debug = __esm({
477
661
  "src/cost-debug.ts"() {
478
662
  "use strict";
663
+ init_storage_limits();
479
664
  LOG_VERSION = 1;
480
665
  }
481
666
  });
@@ -488,6 +673,7 @@ async function runAgentTurn(opts2) {
488
673
  let lastUsage = null;
489
674
  for (let iter = 0; iter < max; iter++) {
490
675
  turn++;
676
+ const previousMessages = opts2.messages.slice();
491
677
  const toolCalls = [];
492
678
  const toolResults = [];
493
679
  let content = "";
@@ -502,7 +688,8 @@ async function runAgentTurn(opts2) {
502
688
  signal: opts2.signal,
503
689
  temperature: opts2.temperature,
504
690
  maxCompletionTokens: opts2.maxCompletionTokens,
505
- reasoningEffort: opts2.reasoningEffort
691
+ reasoningEffort: opts2.reasoningEffort,
692
+ sessionId: opts2.sessionId
506
693
  });
507
694
  for await (const ev of events) {
508
695
  switch (ev.type) {
@@ -561,6 +748,7 @@ async function runAgentTurn(opts2) {
561
748
  sessionId: opts2.sessionId,
562
749
  turn,
563
750
  messages: opts2.messages,
751
+ previousMessages,
564
752
  toolResults,
565
753
  usage: lastUsage
566
754
  });
@@ -588,6 +776,7 @@ async function runAgentTurn(opts2) {
588
776
  sessionId: opts2.sessionId,
589
777
  turn,
590
778
  messages: opts2.messages,
779
+ previousMessages,
591
780
  toolResults,
592
781
  usage: lastUsage
593
782
  });
@@ -832,11 +1021,11 @@ var init_mode = __esm({
832
1021
 
833
1022
  // src/agent/system-prompt.ts
834
1023
  import { platform, release, homedir as homedir3 } from "os";
835
- import { basename, join as join3 } from "path";
1024
+ import { basename, join as join4 } from "path";
836
1025
  import { readFileSync, statSync } from "fs";
837
1026
  function loadContextFile(cwd) {
838
1027
  for (const name of CONTEXT_FILENAMES) {
839
- const path = join3(cwd, name);
1028
+ const path = join4(cwd, name);
840
1029
  try {
841
1030
  const s = statSync(path);
842
1031
  if (!s.isFile() || s.size > MAX_CONTEXT_BYTES) continue;
@@ -847,25 +1036,8 @@ function loadContextFile(cwd) {
847
1036
  }
848
1037
  return null;
849
1038
  }
850
- function buildSystemPrompt(opts2) {
851
- const now2 = opts2.now ?? /* @__PURE__ */ new Date();
852
- const date = now2.toISOString().slice(0, 10);
853
- const shell = process.env.SHELL ? basename(process.env.SHELL) : "sh";
854
- const toolsBlock = opts2.tools.map((t) => {
855
- const perm = t.needsPermission ? " [needs user permission]" : "";
856
- return `- \`${t.name}\`${perm}: ${t.description.split("\n")[0]}`;
857
- }).join("\n");
858
- const base = `You are kimiflare, an interactive coding assistant running in the user's terminal. You act on the user's local filesystem through the tools listed below. You are powered by the ${opts2.model} model on Cloudflare Workers AI.
859
-
860
- Environment:
861
- - Working directory: ${opts2.cwd}
862
- - Platform: ${platform()} ${release()}
863
- - Shell: ${shell}
864
- - Home: ${homedir3()}
865
- - Today: ${date}
866
-
867
- Tools available:
868
- ${toolsBlock}
1039
+ function buildStaticPrefix(opts2) {
1040
+ return `You are kimiflare, an interactive coding assistant running in the user's terminal. You act on the user's local filesystem through the tools listed below. You are powered by the ${opts2.model} model on Cloudflare Workers AI.
869
1041
 
870
1042
  How to work:
871
1043
  - Prefer calling tools over guessing. Read files before editing them. Use \`glob\` and \`grep\` to explore code before assuming structure.
@@ -878,13 +1050,39 @@ How to work:
878
1050
  - If a request is ambiguous, ask one focused question instead of making large assumptions.
879
1051
  - When you finish a task, stop. Do not add a closing summary.
880
1052
  - When creating git commits, you must include \`Co-authored-by: kimiflare <kimiflare@proton.me>\` in the commit message so kimiflare is credited as a contributor. The bash tool will also auto-append this trailer when it detects git commit-creating commands.`;
1053
+ }
1054
+ function buildSessionPrefix(opts2) {
1055
+ const now2 = opts2.now ?? /* @__PURE__ */ new Date();
1056
+ const date = now2.toISOString().slice(0, 10);
1057
+ const shell = process.env.SHELL ? basename(process.env.SHELL) : "sh";
1058
+ const toolsBlock = opts2.tools.map((t) => {
1059
+ const perm = t.needsPermission ? " [needs user permission]" : "";
1060
+ return `- \`${t.name}\`${perm}: ${t.description.split("\n")[0]}`;
1061
+ }).join("\n");
1062
+ const env2 = `Environment:
1063
+ - Working directory: ${opts2.cwd}
1064
+ - Platform: ${platform()} ${release()}
1065
+ - Shell: ${shell}
1066
+ - Home: ${homedir3()}
1067
+ - Today: ${date}`;
1068
+ const tools = `Tools available:
1069
+ ${toolsBlock}`;
881
1070
  const ctx = loadContextFile(opts2.cwd);
882
1071
  const contextBlock = ctx ? `
883
1072
 
884
1073
  Project context from ${ctx.name} (${ctx.lineCount} lines, treat as authoritative):
885
1074
  ${ctx.content.trim()}` : "";
886
1075
  const modeBlock = opts2.mode ? systemPromptForMode(opts2.mode) : "";
887
- return base + contextBlock + modeBlock;
1076
+ return env2 + "\n\n" + tools + contextBlock + modeBlock;
1077
+ }
1078
+ function buildSystemPrompt(opts2) {
1079
+ return buildStaticPrefix(opts2) + "\n\n" + buildSessionPrefix(opts2);
1080
+ }
1081
+ function buildSystemMessages(opts2) {
1082
+ return [
1083
+ { role: "system", content: buildStaticPrefix(opts2) },
1084
+ { role: "system", content: buildSessionPrefix(opts2) }
1085
+ ];
888
1086
  }
889
1087
  var CONTEXT_FILENAMES, MAX_CONTEXT_BYTES;
890
1088
  var init_system_prompt = __esm({
@@ -937,7 +1135,7 @@ var init_paths = __esm({
937
1135
  });
938
1136
 
939
1137
  // src/tools/read.ts
940
- import { readFile as readFile2, stat } from "fs/promises";
1138
+ import { readFile as readFile2, stat as stat2 } from "fs/promises";
941
1139
  var MAX_BYTES, readTool;
942
1140
  var init_read = __esm({
943
1141
  "src/tools/read.ts"() {
@@ -961,7 +1159,7 @@ var init_read = __esm({
961
1159
  render: ({ path }) => ({ title: `read ${collapsePath(path, process.cwd())}` }),
962
1160
  async run(args, ctx) {
963
1161
  const abs = resolvePath(ctx.cwd, args.path);
964
- const st = await stat(abs);
1162
+ const st = await stat2(abs);
965
1163
  if (st.size > MAX_BYTES) throw new Error(`file too large: ${st.size} bytes (max ${MAX_BYTES})`);
966
1164
  const text = await readFile2(abs, "utf8");
967
1165
  const lines = text.split("\n");
@@ -1073,7 +1271,7 @@ var init_edit = __esm({
1073
1271
  // src/tools/bash.ts
1074
1272
  import { spawn } from "child_process";
1075
1273
  import { tmpdir } from "os";
1076
- import { join as join4 } from "path";
1274
+ import { join as join5 } from "path";
1077
1275
  function formatBashTitle(raw) {
1078
1276
  let cmd = (raw ?? "").trim();
1079
1277
  const m = cmd.match(/^cd\s+([^\s&;]+)\s*(?:&&|;)\s*(.*)$/);
@@ -1089,7 +1287,7 @@ function injectCoauthor(command, coauthor) {
1089
1287
  const isRebaseContinue = /\bgit\s+rebase\b/.test(trimmed) && !/\b--abort\b|\b--skip\b/.test(trimmed);
1090
1288
  const mentionsGit = /\bgit\b/.test(trimmed);
1091
1289
  if (!createsCommit && !isRebaseContinue && !mentionsGit) return command;
1092
- const tmpFile = join4(tmpdir(), `kf-coauthor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
1290
+ const tmpFile = join5(tmpdir(), `kf-coauthor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
1093
1291
  const amendBlock = `
1094
1292
  if ! git log -1 --pretty=%B 2>/dev/null | grep -qF "${trailer}"; then
1095
1293
  git log -1 --pretty=%B | git interpret-trailers --trailer "${trailer}" > "${tmpFile}" && git commit --amend -F "${tmpFile}" --no-edit && rm -f "${tmpFile}"
@@ -1600,16 +1798,16 @@ var init_executor = __esm({
1600
1798
  // src/util/update-check.ts
1601
1799
  import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
1602
1800
  import { homedir as homedir5 } from "os";
1603
- import { join as join5, dirname as dirname2 } from "path";
1801
+ import { join as join6, dirname as dirname2 } from "path";
1604
1802
  import { fileURLToPath } from "url";
1605
1803
  function cachePath() {
1606
- const xdg = process.env.XDG_CONFIG_HOME || join5(homedir5(), ".config");
1607
- return join5(xdg, "kimiflare", "update-check.json");
1804
+ const xdg = process.env.XDG_CONFIG_HOME || join6(homedir5(), ".config");
1805
+ return join6(xdg, "kimiflare", "update-check.json");
1608
1806
  }
1609
1807
  async function findPackageJson(startDir) {
1610
1808
  let dir = startDir;
1611
1809
  while (true) {
1612
- const candidate = join5(dir, "package.json");
1810
+ const candidate = join6(dir, "package.json");
1613
1811
  try {
1614
1812
  const raw = await readFile6(candidate, "utf8");
1615
1813
  const parsed = JSON.parse(raw);
@@ -1712,11 +1910,15 @@ function indexOfNthUserFromEnd(messages, n) {
1712
1910
  async function compactMessages(opts2) {
1713
1911
  const keep = opts2.keepLastTurns ?? 4;
1714
1912
  const messages = opts2.messages;
1715
- const systemMsg = messages.find((m) => m.role === "system");
1716
- if (!systemMsg) throw new Error("compact: no system message found");
1913
+ let prefixEnd = 0;
1914
+ while (prefixEnd < messages.length && messages[prefixEnd].role === "system") {
1915
+ prefixEnd++;
1916
+ }
1917
+ const prefix = messages.slice(0, prefixEnd);
1918
+ if (prefix.length === 0) throw new Error("compact: no system message found");
1717
1919
  const cutoffUserIdx = indexOfNthUserFromEnd(messages, keep);
1718
1920
  const firstKeepIdx = cutoffUserIdx >= 0 ? cutoffUserIdx : messages.length;
1719
- const toSummarize = messages.slice(1, firstKeepIdx);
1921
+ const toSummarize = messages.slice(prefixEnd, firstKeepIdx);
1720
1922
  const toKeep = messages.slice(firstKeepIdx);
1721
1923
  if (toSummarize.length === 0) {
1722
1924
  return { summary: "", newMessages: messages, replacedCount: 0 };
@@ -1758,7 +1960,7 @@ ${summary.trim()}`
1758
1960
  };
1759
1961
  return {
1760
1962
  summary: summary.trim(),
1761
- newMessages: [systemMsg, summaryMsg, ...toKeep],
1963
+ newMessages: [...prefix, summaryMsg, ...toKeep],
1762
1964
  replacedCount: toSummarize.length
1763
1965
  };
1764
1966
  }
@@ -1778,6 +1980,413 @@ Do not include speculation. Do not include chat-style pleasantries. Use short bu
1778
1980
  }
1779
1981
  });
1780
1982
 
1983
+ // src/agent/session-state.ts
1984
+ function emptySessionState(task = "") {
1985
+ return {
1986
+ task,
1987
+ user_constraints: [],
1988
+ repo_facts: [],
1989
+ files_touched: [],
1990
+ files_modified: [],
1991
+ confirmed_findings: [],
1992
+ open_questions: [],
1993
+ recent_failures: [],
1994
+ decisions: [],
1995
+ next_actions: [],
1996
+ artifact_index: {}
1997
+ };
1998
+ }
1999
+ function formatRecalledArtifacts(recalled) {
2000
+ if (recalled.length === 0) return "";
2001
+ const lines = ["[recalled artifacts]"];
2002
+ for (const { id, artifact } of recalled) {
2003
+ lines.push(`--- artifact:${id} (${artifact.type} from ${artifact.source}) ---`);
2004
+ lines.push(artifact.raw);
2005
+ }
2006
+ return lines.join("\n");
2007
+ }
2008
+ function serializeSessionState(state) {
2009
+ const lines = [];
2010
+ lines.push(`task: ${state.task || "(none)"}`);
2011
+ if (state.user_constraints.length) lines.push(`constraints:
2012
+ ${state.user_constraints.map((c) => " - " + c).join("\n")}`);
2013
+ if (state.repo_facts.length) lines.push(`repo_facts:
2014
+ ${state.repo_facts.map((f) => " - " + f).join("\n")}`);
2015
+ if (state.files_touched.length) lines.push(`files_touched: ${state.files_touched.join(", ")}`);
2016
+ if (state.files_modified.length) lines.push(`files_modified: ${state.files_modified.join(", ")}`);
2017
+ if (state.confirmed_findings.length) lines.push(`findings:
2018
+ ${state.confirmed_findings.map((f) => " - " + f).join("\n")}`);
2019
+ if (state.open_questions.length) lines.push(`open_questions:
2020
+ ${state.open_questions.map((q) => " - " + q).join("\n")}`);
2021
+ if (state.recent_failures.length) lines.push(`recent_failures:
2022
+ ${state.recent_failures.map((f) => " - " + f).join("\n")}`);
2023
+ if (state.decisions.length) lines.push(`decisions:
2024
+ ${state.decisions.map((d) => " - " + d).join("\n")}`);
2025
+ if (state.next_actions.length) lines.push(`next_actions:
2026
+ ${state.next_actions.map((a) => " - " + a).join("\n")}`);
2027
+ if (Object.keys(state.artifact_index).length) {
2028
+ lines.push("artifact_index:");
2029
+ for (const [id, meta] of Object.entries(state.artifact_index)) {
2030
+ lines.push(` ${id}: [${meta.type}] ${meta.summary}`);
2031
+ }
2032
+ }
2033
+ return lines.join("\n");
2034
+ }
2035
+ function buildSessionStateMessage(state) {
2036
+ return {
2037
+ role: "system",
2038
+ content: `[compiled session state]
2039
+ ${serializeSessionState(state)}`
2040
+ };
2041
+ }
2042
+ var ArtifactStore;
2043
+ var init_session_state = __esm({
2044
+ "src/agent/session-state.ts"() {
2045
+ "use strict";
2046
+ ArtifactStore = class {
2047
+ artifacts = /* @__PURE__ */ new Map();
2048
+ maxArtifacts;
2049
+ maxTotalChars;
2050
+ constructor(opts2) {
2051
+ this.maxArtifacts = opts2?.maxArtifacts ?? 200;
2052
+ this.maxTotalChars = opts2?.maxTotalChars ?? 5e5;
2053
+ }
2054
+ add(a) {
2055
+ while (this.totalChars() + a.raw.length > this.maxTotalChars && this.artifacts.size > 0) {
2056
+ this.evictOldest();
2057
+ }
2058
+ while (this.artifacts.size >= this.maxArtifacts) {
2059
+ this.evictOldest();
2060
+ }
2061
+ this.artifacts.set(a.id, a);
2062
+ }
2063
+ get(id) {
2064
+ return this.artifacts.get(id);
2065
+ }
2066
+ has(id) {
2067
+ return this.artifacts.has(id);
2068
+ }
2069
+ list() {
2070
+ return [...this.artifacts.values()].sort((a, b) => a.ts < b.ts ? -1 : 1);
2071
+ }
2072
+ recall(ids) {
2073
+ const out = [];
2074
+ for (const id of ids) {
2075
+ const a = this.artifacts.get(id);
2076
+ if (a) out.push({ id, artifact: a });
2077
+ }
2078
+ return out;
2079
+ }
2080
+ size() {
2081
+ return this.artifacts.size;
2082
+ }
2083
+ totalChars() {
2084
+ let sum = 0;
2085
+ for (const a of this.artifacts.values()) {
2086
+ sum += a.raw.length;
2087
+ }
2088
+ return sum;
2089
+ }
2090
+ evictOldest() {
2091
+ let oldest;
2092
+ for (const a of this.artifacts.values()) {
2093
+ if (!oldest || a.ts < oldest.ts) oldest = a;
2094
+ }
2095
+ if (oldest) this.artifacts.delete(oldest.id);
2096
+ }
2097
+ };
2098
+ }
2099
+ });
2100
+
2101
+ // src/agent/compaction.ts
2102
+ function approxTokens2(n) {
2103
+ return Math.round(n / 4);
2104
+ }
2105
+ function estimateMessageTokens(m) {
2106
+ let chars = 0;
2107
+ if (typeof m.content === "string") {
2108
+ chars = m.content.length;
2109
+ } else if (Array.isArray(m.content)) {
2110
+ chars = m.content.map((p) => p.type === "text" ? p.text.length : 0).reduce((a, b) => a + b, 0);
2111
+ }
2112
+ if (m.reasoning_content) chars += m.reasoning_content.length;
2113
+ if (m.tool_calls) {
2114
+ for (const tc of m.tool_calls) {
2115
+ chars += tc.function.name.length + tc.function.arguments.length;
2116
+ }
2117
+ }
2118
+ return approxTokens2(chars);
2119
+ }
2120
+ function estimatePromptTokens(messages) {
2121
+ return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
2122
+ }
2123
+ function groupIntoTurns(messages) {
2124
+ const prefix = [];
2125
+ let i = 0;
2126
+ while (i < messages.length && messages[i].role === "system") {
2127
+ prefix.push(messages[i]);
2128
+ i++;
2129
+ }
2130
+ const turns = [];
2131
+ while (i < messages.length) {
2132
+ if (messages[i].role !== "user") {
2133
+ i++;
2134
+ continue;
2135
+ }
2136
+ const user = messages[i];
2137
+ i++;
2138
+ if (i >= messages.length || messages[i].role !== "assistant") {
2139
+ continue;
2140
+ }
2141
+ const assistant = messages[i];
2142
+ i++;
2143
+ const tools = [];
2144
+ while (i < messages.length && messages[i].role === "tool") {
2145
+ tools.push(messages[i]);
2146
+ i++;
2147
+ }
2148
+ turns.push({ user, assistant, tools });
2149
+ }
2150
+ return { prefix, turns };
2151
+ }
2152
+ function makeArtifactId(type, index) {
2153
+ return `${type}_${Date.now()}_${index}`;
2154
+ }
2155
+ function extractArtifactsFromTurn(turn, startIndex, store) {
2156
+ const artifacts = [];
2157
+ const stateDelta = {
2158
+ files_touched: [],
2159
+ files_modified: [],
2160
+ confirmed_findings: [],
2161
+ recent_failures: [],
2162
+ decisions: [],
2163
+ next_actions: []
2164
+ };
2165
+ const toolCalls = turn.assistant.tool_calls ?? [];
2166
+ for (let ti = 0; ti < turn.tools.length; ti++) {
2167
+ const tm = turn.tools[ti];
2168
+ const tc = toolCalls[ti];
2169
+ const name = tm.name ?? tc?.function.name ?? "unknown";
2170
+ const content = typeof tm.content === "string" ? tm.content : "";
2171
+ let type = "tool_result";
2172
+ let summary = `${name} result`;
2173
+ let path;
2174
+ if (name === "read") {
2175
+ type = "read_slice";
2176
+ try {
2177
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2178
+ path = args.path;
2179
+ summary = `read ${path ?? "file"}`;
2180
+ if (path && !stateDelta.files_touched.includes(path)) stateDelta.files_touched.push(path);
2181
+ } catch {
2182
+ summary = "read file";
2183
+ }
2184
+ } else if (name === "bash") {
2185
+ type = "bash_log";
2186
+ try {
2187
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2188
+ const cmd = args.command ?? "";
2189
+ summary = `bash: ${cmd.slice(0, 60)}`;
2190
+ if (content.includes("Error") || content.includes("error") || content.includes("FAIL")) {
2191
+ stateDelta.recent_failures.push(`bash failed: ${cmd.slice(0, 80)}`);
2192
+ }
2193
+ } catch {
2194
+ summary = "bash command";
2195
+ }
2196
+ } else if (name === "grep") {
2197
+ type = "grep_result";
2198
+ summary = `grep results (${content.split("\n").length} lines)`;
2199
+ } else if (name === "web_fetch") {
2200
+ type = "web_fetch";
2201
+ try {
2202
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2203
+ summary = `web_fetch: ${args.url ?? "url"}`;
2204
+ } catch {
2205
+ summary = "web_fetch";
2206
+ }
2207
+ } else if (name === "write" || name === "edit") {
2208
+ try {
2209
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2210
+ path = args.path;
2211
+ if (path && !stateDelta.files_modified.includes(path)) stateDelta.files_modified.push(path);
2212
+ if (path && !stateDelta.files_touched.includes(path)) stateDelta.files_touched.push(path);
2213
+ } catch {
2214
+ }
2215
+ continue;
2216
+ } else if (name === "glob") {
2217
+ try {
2218
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2219
+ summary = `glob: ${args.pattern ?? ""}`;
2220
+ } catch {
2221
+ summary = "glob";
2222
+ }
2223
+ } else if (name === "tasks_set") {
2224
+ try {
2225
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2226
+ const tasks = args.tasks ?? [];
2227
+ const inProgress = tasks.filter((t) => t.status === "in_progress").map((t) => t.title);
2228
+ const pending = tasks.filter((t) => t.status === "pending").map((t) => t.title);
2229
+ if (inProgress.length) stateDelta.next_actions.push(...inProgress);
2230
+ if (pending.length) stateDelta.next_actions.push(...pending);
2231
+ summary = `tasks_set: ${tasks.length} tasks`;
2232
+ } catch {
2233
+ summary = "tasks_set";
2234
+ }
2235
+ }
2236
+ const maxRaw = 5e4;
2237
+ const raw = content.length > maxRaw ? content.slice(0, maxRaw) + `
2238
+ ...[${content.length - maxRaw} chars truncated]` : content;
2239
+ const artifact = {
2240
+ id: makeArtifactId(type, startIndex + ti),
2241
+ type,
2242
+ summary,
2243
+ raw,
2244
+ source: name,
2245
+ path,
2246
+ ts: (/* @__PURE__ */ new Date()).toISOString()
2247
+ };
2248
+ artifacts.push(artifact);
2249
+ if (!content.includes("Error") && !content.includes("error") && content.length > 0 && content.length < 2e3) {
2250
+ stateDelta.confirmed_findings.push(`${name}: ${content.slice(0, 200)}`);
2251
+ }
2252
+ }
2253
+ const assistantText = typeof turn.assistant.content === "string" ? turn.assistant.content : "";
2254
+ if (assistantText.length > 0) {
2255
+ const decisionPatterns = [
2256
+ /(?:decided?|will|plan to|going to|should|need to)\s+(.{10,200})/gi,
2257
+ /(?:let's|let us)\s+(.{10,200})/gi
2258
+ ];
2259
+ for (const pattern of decisionPatterns) {
2260
+ let match;
2261
+ while ((match = pattern.exec(assistantText)) !== null) {
2262
+ const decision = match[1].trim().replace(/\.$/, "");
2263
+ if (decision.length > 10 && !stateDelta.decisions.includes(decision)) {
2264
+ stateDelta.decisions.push(decision);
2265
+ }
2266
+ }
2267
+ }
2268
+ }
2269
+ return { artifacts, stateDelta };
2270
+ }
2271
+ function mergeState(state, delta) {
2272
+ const mergeArr = (a, b) => {
2273
+ if (!b) return a;
2274
+ const set = new Set(a);
2275
+ for (const item of b) set.add(item);
2276
+ return [...set];
2277
+ };
2278
+ return {
2279
+ ...state,
2280
+ task: state.task || delta.task || "",
2281
+ user_constraints: mergeArr(state.user_constraints, delta.user_constraints),
2282
+ repo_facts: mergeArr(state.repo_facts, delta.repo_facts),
2283
+ files_touched: mergeArr(state.files_touched, delta.files_touched),
2284
+ files_modified: mergeArr(state.files_modified, delta.files_modified),
2285
+ confirmed_findings: mergeArr(state.confirmed_findings, delta.confirmed_findings),
2286
+ open_questions: mergeArr(state.open_questions, delta.open_questions),
2287
+ recent_failures: mergeArr(state.recent_failures, delta.recent_failures),
2288
+ decisions: mergeArr(state.decisions, delta.decisions),
2289
+ next_actions: mergeArr(state.next_actions, delta.next_actions),
2290
+ artifact_index: { ...state.artifact_index, ...delta.artifact_index }
2291
+ };
2292
+ }
2293
+ function shouldCompact(opts2) {
2294
+ const tokenThreshold = opts2.tokenThreshold ?? 8e4;
2295
+ const turnThreshold = opts2.turnThreshold ?? 12;
2296
+ const tokens = estimatePromptTokens(opts2.messages);
2297
+ const { turns } = groupIntoTurns(opts2.messages);
2298
+ return tokens > tokenThreshold || turns.length > turnThreshold;
2299
+ }
2300
+ function compactMessages2(opts2) {
2301
+ const keepLastTurns = opts2.keepLastTurns ?? 4;
2302
+ const { prefix, turns } = groupIntoTurns(opts2.messages);
2303
+ const tokensBefore = estimatePromptTokens(opts2.messages);
2304
+ if (turns.length <= keepLastTurns) {
2305
+ return {
2306
+ newMessages: opts2.messages,
2307
+ newState: opts2.state,
2308
+ metrics: {
2309
+ estimatedTokensBefore: tokensBefore,
2310
+ estimatedTokensAfter: tokensBefore,
2311
+ archivedArtifacts: 0,
2312
+ recalledArtifacts: 0,
2313
+ rawTurnsRemoved: 0,
2314
+ rawTurnsKept: turns.length
2315
+ }
2316
+ };
2317
+ }
2318
+ const toCompact = turns.slice(0, turns.length - keepLastTurns);
2319
+ const toKeep = turns.slice(turns.length - keepLastTurns);
2320
+ let newState = { ...opts2.state };
2321
+ let archivedCount = 0;
2322
+ for (let i = 0; i < toCompact.length; i++) {
2323
+ const turn = toCompact[i];
2324
+ const { artifacts, stateDelta } = extractArtifactsFromTurn(turn, i, opts2.store);
2325
+ for (const artifact of artifacts) {
2326
+ opts2.store.add(artifact);
2327
+ archivedCount++;
2328
+ newState.artifact_index[artifact.id] = {
2329
+ type: artifact.type,
2330
+ summary: artifact.summary,
2331
+ source: artifact.source,
2332
+ path: artifact.path
2333
+ };
2334
+ }
2335
+ newState = mergeState(newState, stateDelta);
2336
+ if (!newState.task && typeof turn.user.content === "string") {
2337
+ newState.task = turn.user.content.slice(0, 200);
2338
+ }
2339
+ }
2340
+ const workingMemory = [];
2341
+ for (const turn of toKeep) {
2342
+ workingMemory.push(turn.user);
2343
+ workingMemory.push(turn.assistant);
2344
+ for (const tm of turn.tools) {
2345
+ workingMemory.push(tm);
2346
+ }
2347
+ }
2348
+ const stateMsg = buildSessionStateMessage(newState);
2349
+ const newMessages = [...prefix, stateMsg, ...workingMemory];
2350
+ const tokensAfter = estimatePromptTokens(newMessages);
2351
+ const metrics = {
2352
+ estimatedTokensBefore: tokensBefore,
2353
+ estimatedTokensAfter: tokensAfter,
2354
+ archivedArtifacts: archivedCount,
2355
+ recalledArtifacts: 0,
2356
+ rawTurnsRemoved: toCompact.length,
2357
+ rawTurnsKept: toKeep.length
2358
+ };
2359
+ return { newMessages, newState, metrics };
2360
+ }
2361
+ function recallArtifacts(messages, store, state) {
2362
+ const text = messages.map((m) => typeof m.content === "string" ? m.content : "").join(" ");
2363
+ const ids = [];
2364
+ for (const [id, meta] of Object.entries(state.artifact_index)) {
2365
+ if (meta.path && text.includes(meta.path)) {
2366
+ ids.push(id);
2367
+ }
2368
+ }
2369
+ for (const failure of state.recent_failures) {
2370
+ const keyword = failure.split(":")[0];
2371
+ if (keyword && text.toLowerCase().includes(keyword.toLowerCase())) {
2372
+ for (const [id, meta] of Object.entries(state.artifact_index)) {
2373
+ if (meta.source === "bash" && !ids.includes(id)) {
2374
+ ids.push(id);
2375
+ }
2376
+ }
2377
+ }
2378
+ }
2379
+ const uniqueIds = [...new Set(ids)].slice(0, 5);
2380
+ const recalled = store.recall(uniqueIds);
2381
+ return { ids: uniqueIds, recalled };
2382
+ }
2383
+ var init_compaction = __esm({
2384
+ "src/agent/compaction.ts"() {
2385
+ "use strict";
2386
+ init_session_state();
2387
+ }
2388
+ });
2389
+
1781
2390
  // src/mcp/adapter.ts
1782
2391
  function mcpToolToSpec(serverName, mcpTool, client) {
1783
2392
  const prefix = `mcp_${sanitizeName(serverName)}_`;
@@ -3730,12 +4339,20 @@ var init_theme = __esm({
3730
4339
  });
3731
4340
 
3732
4341
  // src/sessions.ts
3733
- import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5, readdir, stat as stat2 } from "fs/promises";
4342
+ var sessions_exports = {};
4343
+ __export(sessions_exports, {
4344
+ listSessions: () => listSessions,
4345
+ loadSession: () => loadSession,
4346
+ makeSessionId: () => makeSessionId,
4347
+ pruneSessions: () => pruneSessions,
4348
+ saveSession: () => saveSession
4349
+ });
4350
+ import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5, readdir as readdir2, stat as stat3 } from "fs/promises";
3734
4351
  import { homedir as homedir6 } from "os";
3735
- import { join as join6 } from "path";
4352
+ import { join as join7 } from "path";
3736
4353
  function sessionsDir() {
3737
- const xdg = process.env.XDG_DATA_HOME || join6(homedir6(), ".local", "share");
3738
- return join6(xdg, "kimiflare", "sessions");
4354
+ const xdg = process.env.XDG_DATA_HOME || join7(homedir6(), ".local", "share");
4355
+ return join7(xdg, "kimiflare", "sessions");
3739
4356
  }
3740
4357
  function sanitize(text) {
3741
4358
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
@@ -3748,24 +4365,29 @@ function makeSessionId(firstPrompt) {
3748
4365
  async function saveSession(file) {
3749
4366
  const dir = sessionsDir();
3750
4367
  await mkdir5(dir, { recursive: true });
3751
- const path = join6(dir, `${file.id}.json`);
4368
+ const path = join7(dir, `${file.id}.json`);
3752
4369
  await writeFile5(path, JSON.stringify(file, null, 2), "utf8");
3753
4370
  return path;
3754
4371
  }
4372
+ async function pruneSessions() {
4373
+ const dir = sessionsDir();
4374
+ const files = await listFilesByMtime(dir, /\.json$/);
4375
+ return pruneFiles(files, RETENTION.sessionMaxAgeDays, RETENTION.sessionMaxCount);
4376
+ }
3755
4377
  async function listSessions(limit = 30) {
3756
4378
  const dir = sessionsDir();
3757
4379
  let entries;
3758
4380
  try {
3759
- entries = await readdir(dir);
4381
+ entries = await readdir2(dir);
3760
4382
  } catch {
3761
4383
  return [];
3762
4384
  }
3763
4385
  const summaries = [];
3764
4386
  for (const name of entries) {
3765
4387
  if (!name.endsWith(".json")) continue;
3766
- const path = join6(dir, name);
4388
+ const path = join7(dir, name);
3767
4389
  try {
3768
- const [s, raw] = await Promise.all([stat2(path), readFile7(path, "utf8")]);
4390
+ const [s, raw] = await Promise.all([stat3(path), readFile7(path, "utf8")]);
3769
4391
  const parsed = JSON.parse(raw);
3770
4392
  const firstUser = parsed.messages.find((m) => m.role === "user");
3771
4393
  const firstPrompt = typeof firstUser?.content === "string" ? firstUser.content : firstUser?.content ? firstUser.content.find((p) => p.type === "text")?.text ?? "(no prompt)" : "(no prompt)";
@@ -3790,6 +4412,7 @@ async function loadSession(filePath) {
3790
4412
  var init_sessions = __esm({
3791
4413
  "src/sessions.ts"() {
3792
4414
  "use strict";
4415
+ init_storage_limits();
3793
4416
  }
3794
4417
  });
3795
4418
 
@@ -3835,17 +4458,21 @@ var init_image = __esm({
3835
4458
  // src/usage-tracker.ts
3836
4459
  import { readFile as readFile9, writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
3837
4460
  import { homedir as homedir7 } from "os";
3838
- import { join as join7 } from "path";
4461
+ import { join as join8 } from "path";
3839
4462
  function usageDir() {
3840
- const xdg = process.env.XDG_DATA_HOME || join7(homedir7(), ".local", "share");
3841
- return join7(xdg, "kimiflare");
4463
+ const xdg = process.env.XDG_DATA_HOME || join8(homedir7(), ".local", "share");
4464
+ return join8(xdg, "kimiflare");
3842
4465
  }
3843
4466
  function usagePath() {
3844
- return join7(usageDir(), "usage.json");
4467
+ return join8(usageDir(), "usage.json");
3845
4468
  }
3846
4469
  function today() {
3847
4470
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3848
4471
  }
4472
+ function cutoffDate(daysBack) {
4473
+ const d = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1e3);
4474
+ return d.toISOString().slice(0, 10);
4475
+ }
3849
4476
  async function loadLog() {
3850
4477
  try {
3851
4478
  const raw = await readFile9(usagePath(), "utf8");
@@ -3875,8 +4502,18 @@ function getOrCreateSession(log, sessionId, date) {
3875
4502
  }
3876
4503
  return session;
3877
4504
  }
4505
+ function pruneUsageLog(log) {
4506
+ const dayCutoff = cutoffDate(RETENTION.usageDayMaxAgeDays);
4507
+ const sessionCutoff = cutoffDate(RETENTION.usageSessionMaxAgeDays);
4508
+ const days = log.days.filter((d) => d.date >= dayCutoff);
4509
+ let sessions = log.sessions.filter((s) => s.date >= sessionCutoff);
4510
+ if (sessions.length > RETENTION.usageSessionMaxCount) {
4511
+ sessions = sessions.sort((a, b) => b.date < a.date ? -1 : b.date > a.date ? 1 : 0).slice(0, RETENTION.usageSessionMaxCount);
4512
+ }
4513
+ return { ...log, days, sessions };
4514
+ }
3878
4515
  async function recordUsage(sessionId, usage) {
3879
- const log = await loadLog();
4516
+ const log = pruneUsageLog(await loadLog());
3880
4517
  const date = today();
3881
4518
  const cost = calculateCost(usage.prompt_tokens, usage.completion_tokens, usage.prompt_tokens_details?.cached_tokens ?? 0);
3882
4519
  const day = getOrCreateDay(log, date);
@@ -3892,7 +4529,7 @@ async function recordUsage(sessionId, usage) {
3892
4529
  await saveLog(log);
3893
4530
  }
3894
4531
  async function getCostReport(sessionId) {
3895
- const log = await loadLog();
4532
+ const log = pruneUsageLog(await loadLog());
3896
4533
  const date = today();
3897
4534
  const currentMonth = date.slice(0, 7);
3898
4535
  const session = log.sessions.find((s) => s.id === sessionId) ?? { date, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0 };
@@ -3951,6 +4588,7 @@ var init_usage_tracker = __esm({
3951
4588
  "src/usage-tracker.ts"() {
3952
4589
  "use strict";
3953
4590
  init_pricing();
4591
+ init_storage_limits();
3954
4592
  LOG_VERSION2 = 1;
3955
4593
  }
3956
4594
  });
@@ -3963,13 +4601,24 @@ __export(app_exports, {
3963
4601
  import { useState as useState6, useRef as useRef3, useEffect as useEffect4, useCallback } from "react";
3964
4602
  import { Box as Box12, Text as Text13, useApp, useInput as useInput2, render } from "ink";
3965
4603
  import { existsSync } from "fs";
3966
- import { join as join8 } from "path";
3967
- import { unlink } from "fs/promises";
4604
+ import { join as join9 } from "path";
4605
+ import { unlink as unlink2 } from "fs/promises";
3968
4606
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
3969
4607
  function capEvents(prev) {
3970
4608
  if (prev.length <= MAX_EVENTS) return prev;
3971
4609
  return prev.slice(prev.length - MAX_EVENTS);
3972
4610
  }
4611
+ function makePrefixMessages(cacheStable, model, mode, tools) {
4612
+ if (cacheStable) {
4613
+ return buildSystemMessages({ cwd: process.cwd(), tools, model, mode });
4614
+ }
4615
+ return [
4616
+ {
4617
+ role: "system",
4618
+ content: buildSystemPrompt({ cwd: process.cwd(), tools, model, mode })
4619
+ }
4620
+ ];
4621
+ }
3973
4622
  function findImagePaths(text) {
3974
4623
  const paths = [];
3975
4624
  for (const token of text.split(/\s+/)) {
@@ -4017,17 +4666,10 @@ function App({ initialCfg, initialUpdateResult }) {
4017
4666
  const [verbose, setVerbose] = useState6(false);
4018
4667
  const [hasUpdate, setHasUpdate] = useState6(initialUpdateResult?.hasUpdate ?? false);
4019
4668
  const [latestVersion, setLatestVersion] = useState6(initialUpdateResult?.latestVersion ?? null);
4020
- const messagesRef = useRef3([
4021
- {
4022
- role: "system",
4023
- content: buildSystemPrompt({
4024
- cwd: process.cwd(),
4025
- tools: ALL_TOOLS,
4026
- model: cfg?.model ?? DEFAULT_MODEL,
4027
- mode: "edit"
4028
- })
4029
- }
4030
- ]);
4669
+ const cacheStableRef = useRef3(initialCfg?.cacheStablePrompts !== false);
4670
+ const messagesRef = useRef3(
4671
+ makePrefixMessages(cacheStableRef.current, cfg?.model ?? DEFAULT_MODEL, "edit", ALL_TOOLS)
4672
+ );
4031
4673
  const executorRef = useRef3(new ToolExecutor(ALL_TOOLS));
4032
4674
  const activeAsstIdRef = useRef3(null);
4033
4675
  const activeControllerRef = useRef3(null);
@@ -4037,11 +4679,27 @@ function App({ initialCfg, initialUpdateResult }) {
4037
4679
  const tasksRef = useRef3([]);
4038
4680
  const usageRef = useRef3(null);
4039
4681
  const updateCheckedRef = useRef3(false);
4682
+ const sessionStateRef = useRef3(emptySessionState());
4683
+ const artifactStoreRef = useRef3(new ArtifactStore());
4684
+ const compiledContextRef = useRef3(initialCfg?.compiledContext === true);
4040
4685
  const updateNudgedRef = useRef3(false);
4041
4686
  const compactSuggestedRef = useRef3(false);
4042
4687
  const mcpManagerRef = useRef3(new McpManager());
4043
4688
  const mcpToolsRef = useRef3([]);
4044
4689
  const mcpInitRef = useRef3(false);
4690
+ useEffect4(() => {
4691
+ if (!cfg) return;
4692
+ void Promise.resolve().then(() => (init_sessions(), sessions_exports)).then(
4693
+ ({ pruneSessions: pruneSessions2 }) => pruneSessions2().then((removed) => {
4694
+ if (removed > 0) {
4695
+ setEvents((e) => [
4696
+ ...e,
4697
+ { kind: "info", key: mkKey(), text: `pruned ${removed} old session files` }
4698
+ ]);
4699
+ }
4700
+ })
4701
+ );
4702
+ }, [cfg]);
4045
4703
  useEffect4(() => {
4046
4704
  if (!cfg || updateCheckedRef.current) return;
4047
4705
  updateCheckedRef.current = true;
@@ -4095,15 +4753,27 @@ function App({ initialCfg, initialUpdateResult }) {
4095
4753
  }, [cfg, initialUpdateResult]);
4096
4754
  useEffect4(() => {
4097
4755
  modeRef.current = mode;
4098
- messagesRef.current[0] = {
4099
- role: "system",
4100
- content: buildSystemPrompt({
4101
- cwd: process.cwd(),
4102
- tools: [...ALL_TOOLS, ...mcpToolsRef.current],
4103
- model: cfg?.model ?? DEFAULT_MODEL,
4104
- mode
4105
- })
4106
- };
4756
+ if (cacheStableRef.current) {
4757
+ messagesRef.current[1] = {
4758
+ role: "system",
4759
+ content: buildSessionPrefix({
4760
+ cwd: process.cwd(),
4761
+ tools: [...ALL_TOOLS, ...mcpToolsRef.current],
4762
+ model: cfg?.model ?? DEFAULT_MODEL,
4763
+ mode
4764
+ })
4765
+ };
4766
+ } else {
4767
+ messagesRef.current[0] = {
4768
+ role: "system",
4769
+ content: buildSystemPrompt({
4770
+ cwd: process.cwd(),
4771
+ tools: [...ALL_TOOLS, ...mcpToolsRef.current],
4772
+ model: cfg?.model ?? DEFAULT_MODEL,
4773
+ mode
4774
+ })
4775
+ };
4776
+ }
4107
4777
  if (mode === "plan") {
4108
4778
  executorRef.current.clearSessionPermissions();
4109
4779
  }
@@ -4176,15 +4846,27 @@ function App({ initialCfg, initialUpdateResult }) {
4176
4846
  }
4177
4847
  }
4178
4848
  if (totalTools > 0) {
4179
- messagesRef.current[0] = {
4180
- role: "system",
4181
- content: buildSystemPrompt({
4182
- cwd: process.cwd(),
4183
- tools: [...ALL_TOOLS, ...mcpToolsRef.current],
4184
- model: cfg.model ?? DEFAULT_MODEL,
4185
- mode: modeRef.current
4186
- })
4187
- };
4849
+ if (cacheStableRef.current) {
4850
+ messagesRef.current[1] = {
4851
+ role: "system",
4852
+ content: buildSessionPrefix({
4853
+ cwd: process.cwd(),
4854
+ tools: [...ALL_TOOLS, ...mcpToolsRef.current],
4855
+ model: cfg.model ?? DEFAULT_MODEL,
4856
+ mode: modeRef.current
4857
+ })
4858
+ };
4859
+ } else {
4860
+ messagesRef.current[0] = {
4861
+ role: "system",
4862
+ content: buildSystemPrompt({
4863
+ cwd: process.cwd(),
4864
+ tools: [...ALL_TOOLS, ...mcpToolsRef.current],
4865
+ model: cfg.model ?? DEFAULT_MODEL,
4866
+ mode: modeRef.current
4867
+ })
4868
+ };
4869
+ }
4188
4870
  setEvents((e) => [
4189
4871
  ...e,
4190
4872
  { kind: "info", key: mkKey(), text: `MCP connected \u2014 ${totalTools} external tool${totalTools === 1 ? "" : "s"} available` }
@@ -4219,7 +4901,8 @@ function App({ initialCfg, initialUpdateResult }) {
4219
4901
  model: cfg.model,
4220
4902
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4221
4903
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4222
- messages: messagesRef.current
4904
+ messages: messagesRef.current,
4905
+ sessionState: compiledContextRef.current ? sessionStateRef.current : void 0
4223
4906
  });
4224
4907
  } catch {
4225
4908
  }
@@ -4284,29 +4967,55 @@ function App({ initialCfg, initialUpdateResult }) {
4284
4967
  const controller = new AbortController();
4285
4968
  activeControllerRef.current = controller;
4286
4969
  try {
4287
- const result = await compactMessages({
4288
- accountId: cfg.accountId,
4289
- apiToken: cfg.apiToken,
4290
- model: cfg.model,
4291
- messages: messagesRef.current,
4292
- signal: controller.signal
4293
- });
4294
- if (result.replacedCount === 0) {
4295
- setEvents((e) => [
4296
- ...e,
4297
- { kind: "info", key: mkKey(), text: "nothing to compact yet" }
4298
- ]);
4970
+ if (compiledContextRef.current) {
4971
+ const result = compactMessages2({
4972
+ messages: messagesRef.current,
4973
+ state: sessionStateRef.current,
4974
+ store: artifactStoreRef.current
4975
+ });
4976
+ if (result.metrics.rawTurnsRemoved === 0) {
4977
+ setEvents((e) => [
4978
+ ...e,
4979
+ { kind: "info", key: mkKey(), text: "nothing to compact yet" }
4980
+ ]);
4981
+ } else {
4982
+ messagesRef.current = result.newMessages;
4983
+ sessionStateRef.current = result.newState;
4984
+ setEvents((e) => [
4985
+ ...e,
4986
+ {
4987
+ kind: "info",
4988
+ key: mkKey(),
4989
+ text: `compacted ${result.metrics.rawTurnsRemoved} turns \u2192 ${result.metrics.estimatedTokensBefore} \u2192 ${result.metrics.estimatedTokensAfter} tokens, ${result.metrics.archivedArtifacts} artifacts`
4990
+ }
4991
+ ]);
4992
+ await saveSessionSafe();
4993
+ }
4299
4994
  } else {
4300
- messagesRef.current = result.newMessages;
4301
- setEvents((e) => [
4302
- ...e,
4303
- {
4304
- kind: "info",
4305
- key: mkKey(),
4306
- text: `compacted ${result.replacedCount} messages into a summary`
4307
- }
4308
- ]);
4309
- await saveSessionSafe();
4995
+ const result = await compactMessages({
4996
+ accountId: cfg.accountId,
4997
+ apiToken: cfg.apiToken,
4998
+ model: cfg.model,
4999
+ messages: messagesRef.current,
5000
+ signal: controller.signal
5001
+ });
5002
+ if (result.replacedCount === 0) {
5003
+ setEvents((e) => [
5004
+ ...e,
5005
+ { kind: "info", key: mkKey(), text: "nothing to compact yet" }
5006
+ ]);
5007
+ } else {
5008
+ messagesRef.current = result.newMessages;
5009
+ setEvents((e) => [
5010
+ ...e,
5011
+ {
5012
+ kind: "info",
5013
+ key: mkKey(),
5014
+ text: `compacted ${result.replacedCount} messages into a summary`
5015
+ }
5016
+ ]);
5017
+ await saveSessionSafe();
5018
+ }
4310
5019
  }
4311
5020
  } catch (e) {
4312
5021
  if (e.name !== "AbortError") {
@@ -4333,13 +5042,13 @@ function App({ initialCfg, initialUpdateResult }) {
4333
5042
  }
4334
5043
  const cwd = process.cwd();
4335
5044
  for (const name of ["KIMI.md", "KIMIFLARE.md", "AGENT.md"]) {
4336
- if (existsSync(join8(cwd, name))) {
5045
+ if (existsSync(join9(cwd, name))) {
4337
5046
  setEvents((e) => [
4338
5047
  ...e,
4339
5048
  {
4340
5049
  kind: "info",
4341
5050
  key: mkKey(),
4342
- text: `${name} already exists at ${join8(cwd, name)} \u2014 delete it first if you want to regenerate`
5051
+ text: `${name} already exists at ${join9(cwd, name)} \u2014 delete it first if you want to regenerate`
4343
5052
  }
4344
5053
  ]);
4345
5054
  return;
@@ -4456,16 +5165,28 @@ function App({ initialCfg, initialUpdateResult }) {
4456
5165
  })
4457
5166
  }
4458
5167
  });
4459
- if (existsSync(join8(cwd, "KIMI.md"))) {
4460
- messagesRef.current[0] = {
4461
- role: "system",
4462
- content: buildSystemPrompt({
4463
- cwd,
4464
- tools: [...ALL_TOOLS, ...mcpToolsRef.current],
4465
- model: cfg.model,
4466
- mode: modeRef.current
4467
- })
4468
- };
5168
+ if (existsSync(join9(cwd, "KIMI.md"))) {
5169
+ if (cacheStableRef.current) {
5170
+ messagesRef.current[1] = {
5171
+ role: "system",
5172
+ content: buildSessionPrefix({
5173
+ cwd,
5174
+ tools: [...ALL_TOOLS, ...mcpToolsRef.current],
5175
+ model: cfg.model,
5176
+ mode: modeRef.current
5177
+ })
5178
+ };
5179
+ } else {
5180
+ messagesRef.current[0] = {
5181
+ role: "system",
5182
+ content: buildSystemPrompt({
5183
+ cwd,
5184
+ tools: [...ALL_TOOLS, ...mcpToolsRef.current],
5185
+ model: cfg.model,
5186
+ mode: modeRef.current
5187
+ })
5188
+ };
5189
+ }
4469
5190
  setEvents((e) => [
4470
5191
  ...e,
4471
5192
  { kind: "info", key: mkKey(), text: "KIMI.md generated; context loaded for future turns" }
@@ -4493,6 +5214,10 @@ function App({ initialCfg, initialUpdateResult }) {
4493
5214
  const file = await loadSession(picked.filePath);
4494
5215
  messagesRef.current = file.messages;
4495
5216
  sessionIdRef.current = file.id;
5217
+ if (file.sessionState && compiledContextRef.current) {
5218
+ sessionStateRef.current = file.sessionState;
5219
+ artifactStoreRef.current = new ArtifactStore();
5220
+ }
4496
5221
  setEvents([
4497
5222
  {
4498
5223
  kind: "info",
@@ -4549,8 +5274,14 @@ function App({ initialCfg, initialUpdateResult }) {
4549
5274
  return true;
4550
5275
  }
4551
5276
  if (c === "/clear") {
4552
- messagesRef.current = [messagesRef.current[0]];
5277
+ if (cacheStableRef.current && messagesRef.current.length >= 2) {
5278
+ messagesRef.current = [messagesRef.current[0], messagesRef.current[1]];
5279
+ } else {
5280
+ messagesRef.current = [messagesRef.current[0]];
5281
+ }
4553
5282
  sessionIdRef.current = null;
5283
+ sessionStateRef.current = emptySessionState();
5284
+ artifactStoreRef.current = new ArtifactStore();
4554
5285
  setEvents([]);
4555
5286
  setUsage(null);
4556
5287
  setTasks([]);
@@ -4778,7 +5509,7 @@ use: /thinking low | medium | high`
4778
5509
  return true;
4779
5510
  }
4780
5511
  if (c === "/logout") {
4781
- unlink(configPath()).catch(() => {
5512
+ unlink2(configPath()).catch(() => {
4782
5513
  });
4783
5514
  setEvents((e) => [
4784
5515
  ...e,
@@ -4839,6 +5570,17 @@ use: /thinking low | medium | high`
4839
5570
  }
4840
5571
  setEvents((e) => [...e, { kind: "user", key: mkKey(), text: display, images: images.length > 0 ? images : void 0 }]);
4841
5572
  messagesRef.current.push({ role: "user", content });
5573
+ if (compiledContextRef.current) {
5574
+ const { ids, recalled } = recallArtifacts(messagesRef.current, artifactStoreRef.current, sessionStateRef.current);
5575
+ if (recalled.length > 0) {
5576
+ const recalledText = formatRecalledArtifacts(recalled);
5577
+ messagesRef.current.push({ role: "system", content: recalledText });
5578
+ sessionStateRef.current = {
5579
+ ...sessionStateRef.current,
5580
+ artifact_index: { ...sessionStateRef.current.artifact_index }
5581
+ };
5582
+ }
5583
+ }
4842
5584
  setBusy(true);
4843
5585
  setTurnStartedAt(Date.now());
4844
5586
  const controller = new AbortController();
@@ -4948,6 +5690,26 @@ use: /thinking low | medium | high`
4948
5690
  }
4949
5691
  });
4950
5692
  await saveSessionSafe();
5693
+ if (compiledContextRef.current && shouldCompact({ messages: messagesRef.current })) {
5694
+ const result = compactMessages2({
5695
+ messages: messagesRef.current,
5696
+ state: sessionStateRef.current,
5697
+ store: artifactStoreRef.current
5698
+ });
5699
+ if (result.metrics.rawTurnsRemoved > 0) {
5700
+ messagesRef.current = result.newMessages;
5701
+ sessionStateRef.current = result.newState;
5702
+ setEvents((e) => [
5703
+ ...e,
5704
+ {
5705
+ kind: "info",
5706
+ key: mkKey(),
5707
+ text: `auto-compacted: ${result.metrics.estimatedTokensBefore} \u2192 ${result.metrics.estimatedTokensAfter} tokens (${result.metrics.archivedArtifacts} artifacts)`
5708
+ }
5709
+ ]);
5710
+ await saveSessionSafe();
5711
+ }
5712
+ }
4951
5713
  } catch (e) {
4952
5714
  if (e.name === "AbortError") {
4953
5715
  setEvents((es) => [...es, { kind: "info", key: mkKey(), text: "(aborted)" }]);
@@ -5144,6 +5906,8 @@ var init_app = __esm({
5144
5906
  init_loop();
5145
5907
  init_system_prompt();
5146
5908
  init_compact();
5909
+ init_compaction();
5910
+ init_session_state();
5147
5911
  init_executor();
5148
5912
  init_manager();
5149
5913
  init_messages();
@@ -5188,11 +5952,11 @@ init_update_check();
5188
5952
  import { Command } from "commander";
5189
5953
  import { readFileSync as readFileSync2 } from "fs";
5190
5954
  import { fileURLToPath as fileURLToPath2 } from "url";
5191
- import { dirname as dirname3, join as join9 } from "path";
5955
+ import { dirname as dirname3, join as join10 } from "path";
5192
5956
  function readPackageVersion() {
5193
5957
  try {
5194
5958
  const here = dirname3(fileURLToPath2(import.meta.url));
5195
- const pkg = JSON.parse(readFileSync2(join9(here, "..", "package.json"), "utf8"));
5959
+ const pkg = JSON.parse(readFileSync2(join10(here, "..", "package.json"), "utf8"));
5196
5960
  return pkg.version ?? "0.0.0";
5197
5961
  } catch {
5198
5962
  return "0.0.0";