kimiflare 0.15.0 → 0.17.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
@@ -37,6 +37,8 @@ async function loadConfig() {
37
37
  const envCoauthor = readCoauthorEnv();
38
38
  const envCacheStable = process.env.KIMIFLARE_CACHE_STABLE_PROMPTS;
39
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;
40
42
  if (envAccount && envToken) {
41
43
  return {
42
44
  accountId: envAccount,
@@ -47,7 +49,8 @@ async function loadConfig() {
47
49
  coauthor: envCoauthor?.enabled ?? true,
48
50
  coauthorName: envCoauthor?.name,
49
51
  coauthorEmail: envCoauthor?.email,
50
- cacheStablePrompts
52
+ cacheStablePrompts,
53
+ compiledContext
51
54
  };
52
55
  }
53
56
  try {
@@ -64,7 +67,8 @@ async function loadConfig() {
64
67
  coauthorName: envCoauthor?.name ?? parsed.coauthorName,
65
68
  coauthorEmail: envCoauthor?.email ?? parsed.coauthorEmail,
66
69
  mcpServers: parsed.mcpServers,
67
- cacheStablePrompts: parsed.cacheStablePrompts ?? cacheStablePrompts
70
+ cacheStablePrompts: parsed.cacheStablePrompts ?? cacheStablePrompts,
71
+ compiledContext: parsed.compiledContext ?? compiledContext
68
72
  };
69
73
  }
70
74
  } catch {
@@ -406,16 +410,107 @@ var init_registry = __esm({
406
410
  }
407
411
  });
408
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
+
409
504
  // src/cost-debug.ts
410
505
  import { appendFile, mkdir as mkdir2 } from "fs/promises";
411
506
  import { homedir as homedir2 } from "os";
412
- import { join as join2 } from "path";
507
+ import { join as join3 } from "path";
413
508
  function debugDir() {
414
- const xdg = process.env.XDG_DATA_HOME || join2(homedir2(), ".local", "share");
415
- return join2(xdg, "kimiflare");
509
+ const xdg = process.env.XDG_DATA_HOME || join3(homedir2(), ".local", "share");
510
+ return join3(xdg, "kimiflare");
416
511
  }
417
512
  function debugPath() {
418
- return join2(debugDir(), "cost-debug.jsonl");
513
+ return join3(debugDir(), "cost-debug.jsonl");
419
514
  }
420
515
  function now() {
421
516
  return (/* @__PURE__ */ new Date()).toISOString();
@@ -471,6 +566,7 @@ function buildToolStats(results) {
471
566
  }
472
567
  async function logCostDebug(entry) {
473
568
  await mkdir2(debugDir(), { recursive: true });
569
+ await rotateJsonl(debugPath(), RETENTION.costDebugMaxBytes, RETENTION.costDebugRotations);
474
570
  await appendFile(debugPath(), JSON.stringify(entry) + "\n", "utf8");
475
571
  }
476
572
  function serializePrefix(messages) {
@@ -556,13 +652,15 @@ async function logTurnDebug(ctx) {
556
652
  toolTotalRawBytes: toolTotalRaw,
557
653
  toolTotalReducedBytes: toolTotalReduced,
558
654
  toolSavingsPct: toolTotalRaw > 0 ? Math.round((toolTotalRaw - toolTotalReduced) / toolTotalRaw * 100) : 0,
559
- cacheDiagnostics
655
+ cacheDiagnostics,
656
+ compaction: ctx.compaction
560
657
  });
561
658
  }
562
659
  var LOG_VERSION;
563
660
  var init_cost_debug = __esm({
564
661
  "src/cost-debug.ts"() {
565
662
  "use strict";
663
+ init_storage_limits();
566
664
  LOG_VERSION = 1;
567
665
  }
568
666
  });
@@ -774,22 +872,18 @@ function isReadOnlyBash(command) {
774
872
  return false;
775
873
  }
776
874
  }
777
- const argCheck = COMMANDS_NEEDING_ARG_CHECK[cmd];
778
- if (argCheck) {
779
- return argCheck(args);
780
- }
781
875
  return READONLY_COMMANDS.has(cmd);
782
876
  }
783
877
  function systemPromptForMode(m) {
784
878
  if (m === "plan") {
785
- return "\n\nPLAN MODE is active. The user wants you to investigate and produce a plan WITHOUT making any changes. Do not call write, edit, or mutating bash commands. You may use read-only bash commands (e.g., git log, git diff, ls, cat) along with read/glob/grep/web-fetch. At the end, present a concise plan (bullets, files to change, approach). The user will review and then exit plan mode to execute.";
879
+ return "\n\nPLAN MODE is active. The user wants you to investigate and produce a plan WITHOUT making any changes. Do not call write, edit, or mutating bash commands. You may use read-only bash commands (e.g., git log, git diff, ls, cat, grep) along with read/glob/grep/web-fetch. Scripting interpreters (node, python3, ruby, perl, awk) and build/package tools (npm, cargo, go, tsc, jest, etc.) are blocked in plan mode. At the end, present a concise plan (bullets, files to change, approach). The user will review and then exit plan mode to execute.";
786
880
  }
787
881
  if (m === "auto") {
788
882
  return "\n\nAUTO MODE is active. The user has opted into autonomous execution \u2014 every tool call will be auto-approved. Work efficiently, but do not take irreversible destructive actions (rm -rf, git push --force, dropping tables, etc.) without pausing to describe them in chat first. Prefer smaller reversible steps.";
789
883
  }
790
884
  return "";
791
885
  }
792
- var MODES, MUTATING_TOOLS, DANGEROUS_PATTERNS, GIT_READONLY_SUBCOMMANDS, READONLY_COMMANDS, COMMANDS_NEEDING_ARG_CHECK;
886
+ var MODES, MUTATING_TOOLS, DANGEROUS_PATTERNS, GIT_READONLY_SUBCOMMANDS, READONLY_COMMANDS;
793
887
  var init_mode = __esm({
794
888
  "src/mode.ts"() {
795
889
  "use strict";
@@ -860,16 +954,8 @@ var init_mode = __esm({
860
954
  "id",
861
955
  "whoami",
862
956
  "groups",
863
- // Dev tools (version/info only)
864
- "node",
865
- "npx",
866
- "python3",
867
- "ruby",
868
- "perl",
869
957
  // Utilities
870
958
  "jq",
871
- "yq",
872
- "awk",
873
959
  "cut",
874
960
  "tr",
875
961
  "base64",
@@ -892,42 +978,16 @@ var init_mode = __esm({
892
978
  "ss",
893
979
  "lsof"
894
980
  ]);
895
- COMMANDS_NEEDING_ARG_CHECK = {
896
- find: (args) => !args.some((a) => a === "-delete" || a === "-exec"),
897
- sed: (args) => !args.some((a) => a === "-i" || a.startsWith("-i")),
898
- tar: (args) => args[0] === "-tf" || args[0] === "--list",
899
- unzip: (args) => args[0] === "-l",
900
- curl: (args) => !args.some((a) => a === "-o" || a === "-O" || a === "-d" || a === "--data" || a.startsWith("-X")),
901
- wget: (args) => !args.some((a) => a === "-O" || a === "--output-document" || a.startsWith("--post")),
902
- npm: (args) => ["list", "view", "config"].includes(args[0] ?? "") && !(args[0] === "config" && args[1] && !args[1].startsWith("get") && args[1] !== "list"),
903
- tsc: (args) => args.every(
904
- (a) => ["--noEmit", "--version", "--showConfig", "--help", "-h", "--init"].includes(a)
905
- ),
906
- eslint: (args) => args.every(
907
- (a) => ["--version", "--print-config", "--help", "-h"].includes(a) || !a.startsWith("-")
908
- ),
909
- prettier: (args) => args.every(
910
- (a) => ["--version", "--check", "--help", "-h"].includes(a) || !a.startsWith("-")
911
- ),
912
- jest: (args) => args.every(
913
- (a) => ["--version", "--listTests", "--showConfig", "--help", "-h"].includes(a) || !a.startsWith("-")
914
- ),
915
- vitest: (args) => args.every(
916
- (a) => ["--version", "--help", "-h"].includes(a) || !a.startsWith("-")
917
- ),
918
- go: (args) => ["version", "env", "list", "mod"].includes(args[0] ?? "") && !(args[0] === "mod" && args[1] && !["graph", "download", "why", "verify"].includes(args[1])),
919
- cargo: (args) => ["--version", "-V", "check", "test", "metadata"].includes(args[0] ?? "") && !(args[0] === "test" && args.includes("--no-run") === false)
920
- };
921
981
  }
922
982
  });
923
983
 
924
984
  // src/agent/system-prompt.ts
925
985
  import { platform, release, homedir as homedir3 } from "os";
926
- import { basename, join as join3 } from "path";
986
+ import { basename, join as join4 } from "path";
927
987
  import { readFileSync, statSync } from "fs";
928
988
  function loadContextFile(cwd) {
929
989
  for (const name of CONTEXT_FILENAMES) {
930
- const path = join3(cwd, name);
990
+ const path = join4(cwd, name);
931
991
  try {
932
992
  const s = statSync(path);
933
993
  if (!s.isFile() || s.size > MAX_CONTEXT_BYTES) continue;
@@ -951,7 +1011,12 @@ How to work:
951
1011
  - You have a 262k-token context window. Read as much of a file as needed rather than guessing.
952
1012
  - If a request is ambiguous, ask one focused question instead of making large assumptions.
953
1013
  - When you finish a task, stop. Do not add a closing summary.
954
- - 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.`;
1014
+ - 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.
1015
+
1016
+ Tool output reduction:
1017
+ - Large tool outputs (grep, read, bash, web_fetch) are reduced to compact summaries by default to preserve context window.
1018
+ - When you see "[output reduced]" with an artifact ID, you can call \`expand_artifact\` with that ID to retrieve the full raw output if you need more detail.
1019
+ - You can also re-run the original tool with more targeted parameters (e.g. read with offset/limit, grep with output_mode="files") instead of expanding.`;
955
1020
  }
956
1021
  function buildSessionPrefix(opts2) {
957
1022
  const now2 = opts2.now ?? /* @__PURE__ */ new Date();
@@ -1005,11 +1070,6 @@ function resolvePath(cwd, input) {
1005
1070
  }
1006
1071
  return isAbsolute(input) ? input : resolve(cwd, input);
1007
1072
  }
1008
- function truncate(s, n) {
1009
- if (s.length <= n) return s;
1010
- return s.slice(0, n) + `
1011
- ... [truncated, ${s.length - n} chars omitted]`;
1012
- }
1013
1073
  function collapsePath(input, cwd, maxLen = 40) {
1014
1074
  if (!input) return input;
1015
1075
  let abs;
@@ -1037,7 +1097,7 @@ var init_paths = __esm({
1037
1097
  });
1038
1098
 
1039
1099
  // src/tools/read.ts
1040
- import { readFile as readFile2, stat } from "fs/promises";
1100
+ import { readFile as readFile2, stat as stat2 } from "fs/promises";
1041
1101
  var MAX_BYTES, readTool;
1042
1102
  var init_read = __esm({
1043
1103
  "src/tools/read.ts"() {
@@ -1046,7 +1106,7 @@ var init_read = __esm({
1046
1106
  MAX_BYTES = 2 * 1024 * 1024;
1047
1107
  readTool = {
1048
1108
  name: "read",
1049
- description: "Read a text file from the local filesystem. Supports optional line offset/limit. Refuses files larger than 2MB. Returns contents with 1-indexed line numbers prefixed, cat -n style.",
1109
+ description: "Read a text file from the local filesystem. Supports optional line offset/limit. Refuses files larger than 2MB. Returns contents with 1-indexed line numbers prefixed, cat -n style. When reading a full file without offset/limit, the output is reduced to a compact outline (imports, exports, signatures, preview) by default; use expand_artifact to retrieve the full content or specify offset/limit for a targeted slice.",
1050
1110
  parameters: {
1051
1111
  type: "object",
1052
1112
  properties: {
@@ -1061,7 +1121,7 @@ var init_read = __esm({
1061
1121
  render: ({ path }) => ({ title: `read ${collapsePath(path, process.cwd())}` }),
1062
1122
  async run(args, ctx) {
1063
1123
  const abs = resolvePath(ctx.cwd, args.path);
1064
- const st = await stat(abs);
1124
+ const st = await stat2(abs);
1065
1125
  if (st.size > MAX_BYTES) throw new Error(`file too large: ${st.size} bytes (max ${MAX_BYTES})`);
1066
1126
  const text = await readFile2(abs, "utf8");
1067
1127
  const lines = text.split("\n");
@@ -1173,7 +1233,7 @@ var init_edit = __esm({
1173
1233
  // src/tools/bash.ts
1174
1234
  import { spawn } from "child_process";
1175
1235
  import { tmpdir } from "os";
1176
- import { join as join4 } from "path";
1236
+ import { join as join5 } from "path";
1177
1237
  function formatBashTitle(raw) {
1178
1238
  let cmd = (raw ?? "").trim();
1179
1239
  const m = cmd.match(/^cd\s+([^\s&;]+)\s*(?:&&|;)\s*(.*)$/);
@@ -1189,7 +1249,7 @@ function injectCoauthor(command, coauthor) {
1189
1249
  const isRebaseContinue = /\bgit\s+rebase\b/.test(trimmed) && !/\b--abort\b|\b--skip\b/.test(trimmed);
1190
1250
  const mentionsGit = /\bgit\b/.test(trimmed);
1191
1251
  if (!createsCommit && !isRebaseContinue && !mentionsGit) return command;
1192
- const tmpFile = join4(tmpdir(), `kf-coauthor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
1252
+ const tmpFile = join5(tmpdir(), `kf-coauthor-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
1193
1253
  const amendBlock = `
1194
1254
  if ! git log -1 --pretty=%B 2>/dev/null | grep -qF "${trailer}"; then
1195
1255
  git log -1 --pretty=%B | git interpret-trailers --trailer "${trailer}" > "${tmpFile}" && git commit --amend -F "${tmpFile}" --no-edit && rm -f "${tmpFile}"
@@ -1242,26 +1302,23 @@ ${stdout.trimEnd()}`);
1242
1302
  ${stderr.trimEnd()}`);
1243
1303
  if (!stdout && !stderr) parts.push("(no output)");
1244
1304
  const raw = parts.join("\n");
1245
- const reduced = truncate(raw, OUTPUT_CAP);
1246
1305
  resolve2({
1247
- content: reduced,
1306
+ content: raw,
1248
1307
  rawBytes: Buffer.byteLength(raw, "utf8"),
1249
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1308
+ reducedBytes: Buffer.byteLength(raw, "utf8")
1250
1309
  });
1251
1310
  });
1252
1311
  });
1253
1312
  }
1254
- var DEFAULT_TIMEOUT, MAX_TIMEOUT, OUTPUT_CAP, bashTool;
1313
+ var DEFAULT_TIMEOUT, MAX_TIMEOUT, bashTool;
1255
1314
  var init_bash = __esm({
1256
1315
  "src/tools/bash.ts"() {
1257
1316
  "use strict";
1258
- init_paths();
1259
1317
  DEFAULT_TIMEOUT = 12e4;
1260
1318
  MAX_TIMEOUT = 6e5;
1261
- OUTPUT_CAP = 3e4;
1262
1319
  bashTool = {
1263
1320
  name: "bash",
1264
- description: "Run a shell command via `bash -lc`. Prompts the user for permission before executing. stdout and stderr are captured, combined, and capped at 30KB.",
1321
+ description: "Run a shell command via `bash -lc`. Prompts the user for permission before executing. stdout and stderr are captured and combined. Large outputs are reduced to a compact summary by default; use expand_artifact to retrieve the full log.",
1265
1322
  parameters: {
1266
1323
  type: "object",
1267
1324
  properties: {
@@ -1346,11 +1403,10 @@ async function runRipgrep(args, root, mode) {
1346
1403
  const { stdout } = await pExecFile("rg", rgArgs, { maxBuffer: 10 * 1024 * 1024 });
1347
1404
  const trimmed = stdout.trim();
1348
1405
  if (!trimmed) return { content: "(no matches)", rawBytes: 0, reducedBytes: 0 };
1349
- const reduced = truncate(trimmed, 3e4);
1350
1406
  return {
1351
- content: reduced,
1407
+ content: trimmed,
1352
1408
  rawBytes: Buffer.byteLength(trimmed, "utf8"),
1353
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1409
+ reducedBytes: Buffer.byteLength(trimmed, "utf8")
1354
1410
  };
1355
1411
  } catch (e) {
1356
1412
  const err = e;
@@ -1389,11 +1445,10 @@ async function runJsFallback(args, root, mode) {
1389
1445
  }
1390
1446
  if (!out.length) return { content: "(no matches)", rawBytes: 0, reducedBytes: 0 };
1391
1447
  const raw = out.join("\n");
1392
- const reduced = truncate(raw, 3e4);
1393
1448
  return {
1394
- content: reduced,
1449
+ content: raw,
1395
1450
  rawBytes: Buffer.byteLength(raw, "utf8"),
1396
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1451
+ reducedBytes: Buffer.byteLength(raw, "utf8")
1397
1452
  };
1398
1453
  }
1399
1454
  var pExecFile, cachedHasRg, grepTool;
@@ -1436,17 +1491,15 @@ var init_grep = __esm({
1436
1491
 
1437
1492
  // src/tools/web-fetch.ts
1438
1493
  import TurndownService from "turndown";
1439
- var MAX_BYTES2, MAX_OUTPUT, TIMEOUT_MS, webFetchTool;
1494
+ var MAX_BYTES2, TIMEOUT_MS, webFetchTool;
1440
1495
  var init_web_fetch = __esm({
1441
1496
  "src/tools/web-fetch.ts"() {
1442
1497
  "use strict";
1443
- init_paths();
1444
1498
  MAX_BYTES2 = 1 * 1024 * 1024;
1445
- MAX_OUTPUT = 1e5;
1446
1499
  TIMEOUT_MS = 2e4;
1447
1500
  webFetchTool = {
1448
1501
  name: "web_fetch",
1449
- description: "Fetch a URL over HTTPS and return its content. HTML pages are converted to markdown. Output is capped at ~100KB.",
1502
+ description: "Fetch a URL over HTTPS and return its content. HTML pages are converted to markdown. Large pages are reduced to a summary by default; use expand_artifact to retrieve the full content.",
1450
1503
  parameters: {
1451
1504
  type: "object",
1452
1505
  properties: {
@@ -1480,11 +1533,10 @@ ${td.turndown(bounded)}`;
1480
1533
 
1481
1534
  ${bounded}`;
1482
1535
  }
1483
- const reduced = truncate(raw, MAX_OUTPUT);
1484
1536
  return {
1485
- content: reduced,
1537
+ content: raw,
1486
1538
  rawBytes: Buffer.byteLength(raw, "utf8"),
1487
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1539
+ reducedBytes: Buffer.byteLength(raw, "utf8")
1488
1540
  };
1489
1541
  } finally {
1490
1542
  clearTimeout(timer);
@@ -1576,6 +1628,423 @@ var init_tasks = __esm({
1576
1628
  }
1577
1629
  });
1578
1630
 
1631
+ // src/tools/artifact-store.ts
1632
+ var ToolArtifactStore;
1633
+ var init_artifact_store = __esm({
1634
+ "src/tools/artifact-store.ts"() {
1635
+ "use strict";
1636
+ ToolArtifactStore = class {
1637
+ artifacts = /* @__PURE__ */ new Map();
1638
+ nextId = 0;
1639
+ maxArtifacts;
1640
+ maxTotalChars;
1641
+ constructor(opts2) {
1642
+ this.maxArtifacts = opts2?.maxArtifacts ?? 500;
1643
+ this.maxTotalChars = opts2?.maxTotalChars ?? 2e6;
1644
+ }
1645
+ /** Store raw content and return a stable artifact ID. */
1646
+ store(raw) {
1647
+ const id = `art_${++this.nextId}`;
1648
+ while (this.totalChars() + raw.length > this.maxTotalChars && this.artifacts.size > 0) {
1649
+ this.evictOldest();
1650
+ }
1651
+ while (this.artifacts.size >= this.maxArtifacts && this.artifacts.size > 0) {
1652
+ this.evictOldest();
1653
+ }
1654
+ this.artifacts.set(id, raw);
1655
+ return id;
1656
+ }
1657
+ retrieve(id) {
1658
+ return this.artifacts.get(id);
1659
+ }
1660
+ has(id) {
1661
+ return this.artifacts.has(id);
1662
+ }
1663
+ clear() {
1664
+ this.artifacts.clear();
1665
+ this.nextId = 0;
1666
+ }
1667
+ size() {
1668
+ return this.artifacts.size;
1669
+ }
1670
+ totalChars() {
1671
+ let sum = 0;
1672
+ for (const raw of this.artifacts.values()) {
1673
+ sum += raw.length;
1674
+ }
1675
+ return sum;
1676
+ }
1677
+ evictOldest() {
1678
+ const first = this.artifacts.keys().next().value;
1679
+ if (first !== void 0) {
1680
+ this.artifacts.delete(first);
1681
+ }
1682
+ }
1683
+ };
1684
+ }
1685
+ });
1686
+
1687
+ // src/tools/reducer.ts
1688
+ function reduceToolOutput(toolName, raw, args, store, config = DEFAULT_REDUCER_CONFIG) {
1689
+ const rawBytes = Buffer.byteLength(raw, "utf8");
1690
+ const artifactId = store.store(raw);
1691
+ if (!config.enabled) {
1692
+ return { content: raw, rawBytes, reducedBytes: rawBytes, artifactId };
1693
+ }
1694
+ let reduced;
1695
+ let wasReduced = false;
1696
+ let hint;
1697
+ switch (toolName) {
1698
+ case "grep": {
1699
+ const r = reduceGrep(raw, args, config.grep);
1700
+ reduced = r.body;
1701
+ wasReduced = r.wasReduced;
1702
+ hint = r.hint;
1703
+ break;
1704
+ }
1705
+ case "read": {
1706
+ const r = reduceRead(raw, args, config.read);
1707
+ reduced = r.body;
1708
+ wasReduced = r.wasReduced;
1709
+ hint = r.hint;
1710
+ break;
1711
+ }
1712
+ case "bash": {
1713
+ const r = reduceBash(raw, args, config.bash);
1714
+ reduced = r.body;
1715
+ wasReduced = r.wasReduced;
1716
+ hint = r.hint;
1717
+ break;
1718
+ }
1719
+ case "web_fetch": {
1720
+ const r = reduceWebFetch(raw, args, config.webFetch);
1721
+ reduced = r.body;
1722
+ wasReduced = r.wasReduced;
1723
+ hint = r.hint;
1724
+ break;
1725
+ }
1726
+ default:
1727
+ reduced = raw;
1728
+ break;
1729
+ }
1730
+ if (!wasReduced) {
1731
+ return { content: reduced, rawBytes, reducedBytes: rawBytes, artifactId };
1732
+ }
1733
+ const footer = `[output reduced \u2014 full raw stored as artifact ${artifactId}]`;
1734
+ const content = hint ? `${reduced}
1735
+ ${footer}
1736
+ ${hint}` : `${reduced}
1737
+ ${footer}`;
1738
+ const reducedBytes = Buffer.byteLength(content, "utf8");
1739
+ return { content, rawBytes, reducedBytes, artifactId };
1740
+ }
1741
+ function parseGrepLines(raw) {
1742
+ const matches = [];
1743
+ for (const line of raw.split("\n")) {
1744
+ const trimmed = line.trim();
1745
+ if (!trimmed) continue;
1746
+ const m = trimmed.match(/^(.+?):(\d+)?:(.*)$/);
1747
+ if (m) {
1748
+ matches.push({ file: m[1], line: m[2] ? parseInt(m[2], 10) : 0, text: m[3] });
1749
+ } else {
1750
+ matches.push({ file: trimmed, line: 0, text: "" });
1751
+ }
1752
+ }
1753
+ return matches;
1754
+ }
1755
+ function reduceGrep(raw, args, cfg) {
1756
+ const isFilesMode = args.output_mode === "files";
1757
+ const matches = parseGrepLines(raw);
1758
+ if (matches.length === 0) {
1759
+ return { body: raw, wasReduced: false };
1760
+ }
1761
+ if (isFilesMode) {
1762
+ const files = [...new Set(matches.map((m) => m.file))];
1763
+ const lines2 = [`${files.length} file(s) matched:`, ...files];
1764
+ return {
1765
+ body: lines2.join("\n"),
1766
+ wasReduced: true,
1767
+ hint: 'Re-run with output_mode="content" for match details.'
1768
+ };
1769
+ }
1770
+ const byFile = /* @__PURE__ */ new Map();
1771
+ for (const m of matches) {
1772
+ const list = byFile.get(m.file) ?? [];
1773
+ list.push(m);
1774
+ byFile.set(m.file, list);
1775
+ }
1776
+ const lines = [];
1777
+ let totalShown = 0;
1778
+ const totalHits = matches.length;
1779
+ const fileCount = byFile.size;
1780
+ lines.push(`Matched ${fileCount} file(s) (${totalHits} total hits):`);
1781
+ for (const [file, hits] of byFile) {
1782
+ if (totalShown >= cfg.maxTotalLines) break;
1783
+ lines.push(` ${file}: ${hits.length} hit(s)`);
1784
+ const toShow = Math.min(hits.length, cfg.maxMatchesPerFile);
1785
+ for (let i = 0; i < toShow; i++) {
1786
+ const h = hits[i];
1787
+ const text = h.text.length > cfg.maxLineLength ? h.text.slice(0, cfg.maxLineLength) + "\u2026" : h.text;
1788
+ const prefix = h.line > 0 ? ` ${h.line}:` : " ";
1789
+ lines.push(`${prefix}${text}`);
1790
+ totalShown++;
1791
+ if (totalShown >= cfg.maxTotalLines) break;
1792
+ }
1793
+ }
1794
+ if (totalShown < totalHits) {
1795
+ lines.push(` \u2026 (${totalHits - totalShown} more hits omitted)`);
1796
+ }
1797
+ return {
1798
+ body: lines.join("\n"),
1799
+ wasReduced: totalHits > totalShown || fileCount > 1,
1800
+ hint: 'Use expand_artifact for full matches, or re-run with output_mode="files" for paths only.'
1801
+ };
1802
+ }
1803
+ function reduceRead(raw, args, cfg) {
1804
+ const hasSlice = typeof args.offset === "number" || typeof args.limit === "number";
1805
+ if (hasSlice) {
1806
+ const lines = raw.split("\n");
1807
+ if (lines.length > cfg.maxSliceLines) {
1808
+ const kept = lines.slice(0, cfg.maxSliceLines).join("\n");
1809
+ return {
1810
+ body: kept,
1811
+ wasReduced: true,
1812
+ hint: `\u2026 (${lines.length - cfg.maxSliceLines} more lines omitted)`
1813
+ };
1814
+ }
1815
+ return { body: raw, wasReduced: false };
1816
+ }
1817
+ const allLines = raw.split("\n");
1818
+ const totalLines = allLines.length;
1819
+ const cleanLines = allLines.map((l) => l.replace(/^\s*\d+\t/, ""));
1820
+ const imports = [];
1821
+ const exports = [];
1822
+ const functions = [];
1823
+ const classes = [];
1824
+ for (let i = 0; i < cleanLines.length; i++) {
1825
+ const line = cleanLines[i];
1826
+ const lineNum = i + 1;
1827
+ if (/^import\s+/.test(line)) {
1828
+ imports.push(`${lineNum}: ${line.trim()}`);
1829
+ } else if (/^(?:export\s+)?class\s+\w+/.test(line)) {
1830
+ classes.push(`${lineNum}: ${line.trim()}`);
1831
+ } else if (/^export\s+/.test(line)) {
1832
+ exports.push(`${lineNum}: ${line.trim()}`);
1833
+ } else if (/^(?:async\s+)?function\s+\w+/.test(line)) {
1834
+ functions.push(`${lineNum}: ${line.trim()}`);
1835
+ }
1836
+ }
1837
+ const parts = [];
1838
+ parts.push(`File: ${totalLines} lines total`);
1839
+ if (imports.length > 0) {
1840
+ parts.push(`
1841
+ Imports (${imports.length}):`);
1842
+ parts.push(...imports.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1843
+ }
1844
+ if (exports.length > 0) {
1845
+ parts.push(`
1846
+ Exports (${exports.length}):`);
1847
+ parts.push(...exports.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1848
+ }
1849
+ if (functions.length > 0) {
1850
+ parts.push(`
1851
+ Functions (${functions.length}):`);
1852
+ parts.push(...functions.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1853
+ }
1854
+ if (classes.length > 0) {
1855
+ parts.push(`
1856
+ Classes (${classes.length}):`);
1857
+ parts.push(...classes.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1858
+ }
1859
+ const previewCount = Math.min(cfg.maxPreviewLines, totalLines);
1860
+ parts.push(`
1861
+ Preview (lines 1\u2013${previewCount}):`);
1862
+ parts.push(...allLines.slice(0, previewCount));
1863
+ return {
1864
+ body: parts.join("\n"),
1865
+ wasReduced: true,
1866
+ hint: "Use expand_artifact for full file, or read with offset/limit for a specific slice."
1867
+ };
1868
+ }
1869
+ function reduceBash(raw, _args, cfg) {
1870
+ const lines = raw.split("\n");
1871
+ if (lines.length <= cfg.maxTotalLines) {
1872
+ return { body: raw, wasReduced: false };
1873
+ }
1874
+ let header = "";
1875
+ let bodyStart = 0;
1876
+ if (lines[0]?.startsWith("exit=") || lines[0]?.startsWith("(timed out")) {
1877
+ header = lines[0];
1878
+ bodyStart = 1;
1879
+ }
1880
+ const body = lines.slice(bodyStart);
1881
+ const isFailure = header.includes("exit=1") || raw.includes("Error:") || raw.includes("error:") || raw.includes("FAIL") || raw.includes("failed");
1882
+ const out = [header];
1883
+ if (isFailure) {
1884
+ const errorIndices = [];
1885
+ for (let i = 0; i < body.length; i++) {
1886
+ const line = body[i];
1887
+ if (/\bError\b/i.test(line) || /\berror\b/i.test(line) || /\bFAIL\b/i.test(line) || /\bfailed\b/i.test(line) || /^\s+at\s+/.test(line) || /\s+Error:\s+/.test(line)) {
1888
+ for (let j = Math.max(0, i - 2); j <= Math.min(body.length - 1, i + 2); j++) {
1889
+ if (!errorIndices.includes(j)) errorIndices.push(j);
1890
+ }
1891
+ }
1892
+ }
1893
+ errorIndices.sort((a, b) => a - b);
1894
+ const cappedError = errorIndices.slice(0, cfg.maxErrorBlockLines);
1895
+ if (cappedError.length > 0) {
1896
+ out.push("--- error block ---");
1897
+ for (const idx of cappedError) {
1898
+ out.push(body[idx]);
1899
+ }
1900
+ }
1901
+ const testNames = [];
1902
+ for (const line of body) {
1903
+ const m = line.match(/(?:✗|✕|×|FAIL)\s+(.+)/) || line.match(/failing\s*\d*\s*:?\s*(.+)/i) || line.match(/Test\s+\w+\s+failed/i);
1904
+ if (m && m[1]) {
1905
+ const name = m[1].trim().slice(0, 120);
1906
+ if (!testNames.includes(name)) testNames.push(name);
1907
+ }
1908
+ }
1909
+ if (testNames.length > 0) {
1910
+ out.push("--- failing tests ---");
1911
+ out.push(...testNames.slice(0, 10));
1912
+ }
1913
+ }
1914
+ const trailing = body.slice(-cfg.maxTrailingLines);
1915
+ out.push("--- last lines ---");
1916
+ out.push(...trailing);
1917
+ let result = out.join("\n");
1918
+ if (cfg.dedupeConsecutiveLines) {
1919
+ result = dedupeConsecutive(result);
1920
+ }
1921
+ return {
1922
+ body: result,
1923
+ wasReduced: true,
1924
+ hint: "Use expand_artifact for full output."
1925
+ };
1926
+ }
1927
+ function dedupeConsecutive(text) {
1928
+ const lines = text.split("\n");
1929
+ const out = [];
1930
+ let repeatCount = 1;
1931
+ for (let i = 0; i < lines.length; i++) {
1932
+ const line = lines[i];
1933
+ const next = lines[i + 1];
1934
+ if (next !== void 0 && next === line) {
1935
+ repeatCount++;
1936
+ continue;
1937
+ }
1938
+ if (repeatCount > 2) {
1939
+ out.push(line);
1940
+ out.push(`\u2026 (${repeatCount - 1} identical lines omitted)`);
1941
+ } else {
1942
+ for (let j = 0; j < repeatCount; j++) {
1943
+ out.push(line);
1944
+ }
1945
+ }
1946
+ repeatCount = 1;
1947
+ }
1948
+ return out.join("\n");
1949
+ }
1950
+ function reduceWebFetch(raw, args, cfg) {
1951
+ const url = typeof args.url === "string" ? args.url : "(unknown URL)";
1952
+ const titleMatch = raw.match(/^#\s+(.+)$/m);
1953
+ const title = titleMatch ? titleMatch[1].trim() : "(no title)";
1954
+ const parts = [];
1955
+ parts.push(`Title: ${title}`);
1956
+ parts.push(`URL: ${url}`);
1957
+ const headings = raw.match(/^#{1,3}\s+.+$/gm) ?? [];
1958
+ if (headings.length > 0) {
1959
+ parts.push("\nSections:");
1960
+ for (const h of headings.slice(0, 10)) {
1961
+ parts.push(` ${h}`);
1962
+ }
1963
+ }
1964
+ const bodyStart = raw.indexOf("\n\n");
1965
+ const body = bodyStart > 0 ? raw.slice(bodyStart + 2) : raw;
1966
+ const excerpt = body.slice(0, cfg.maxChars).trim();
1967
+ if (excerpt) {
1968
+ parts.push(`
1969
+ Excerpt (${excerpt.length} chars):`);
1970
+ parts.push(excerpt);
1971
+ }
1972
+ if (body.length > cfg.maxChars) {
1973
+ parts.push(`
1974
+ \u2026 (${body.length - cfg.maxChars} more chars omitted)`);
1975
+ }
1976
+ return {
1977
+ body: parts.join("\n"),
1978
+ wasReduced: true,
1979
+ hint: "Use expand_artifact for full page content."
1980
+ };
1981
+ }
1982
+ var DEFAULT_REDUCER_CONFIG;
1983
+ var init_reducer = __esm({
1984
+ "src/tools/reducer.ts"() {
1985
+ "use strict";
1986
+ DEFAULT_REDUCER_CONFIG = {
1987
+ enabled: true,
1988
+ grep: {
1989
+ maxTotalLines: 50,
1990
+ maxMatchesPerFile: 3,
1991
+ maxLineLength: 200,
1992
+ maxOutputChars: 3e3
1993
+ },
1994
+ read: {
1995
+ maxOutlineLines: 60,
1996
+ maxSliceLines: 200,
1997
+ maxPreviewLines: 30,
1998
+ maxOutputChars: 4e3
1999
+ },
2000
+ bash: {
2001
+ maxTotalLines: 40,
2002
+ maxErrorBlockLines: 20,
2003
+ maxTrailingLines: 20,
2004
+ maxOutputChars: 4e3,
2005
+ dedupeConsecutiveLines: true
2006
+ },
2007
+ webFetch: {
2008
+ maxChars: 2e3,
2009
+ maxHeadingChars: 500
2010
+ }
2011
+ };
2012
+ }
2013
+ });
2014
+
2015
+ // src/tools/expand-artifact.ts
2016
+ function makeExpandArtifactTool(store) {
2017
+ return {
2018
+ name: "expand_artifact",
2019
+ description: "Retrieve the full raw content of a previously reduced tool output by its artifact ID. Use this when the compact summary is insufficient and you need the complete original output.",
2020
+ parameters: {
2021
+ type: "object",
2022
+ properties: {
2023
+ artifact_id: {
2024
+ type: "string",
2025
+ description: "The artifact ID from a reduced tool output footer, e.g. art_42."
2026
+ }
2027
+ },
2028
+ required: ["artifact_id"],
2029
+ additionalProperties: false
2030
+ },
2031
+ needsPermission: false,
2032
+ render: (args) => ({ title: `expand ${args.artifact_id}` }),
2033
+ run: async (args) => {
2034
+ const raw = store.retrieve(args.artifact_id);
2035
+ if (!raw) {
2036
+ return `Artifact "${args.artifact_id}" not found. It may have been evicted from memory. Re-run the original tool to regenerate the output.`;
2037
+ }
2038
+ return raw;
2039
+ }
2040
+ };
2041
+ }
2042
+ var init_expand_artifact = __esm({
2043
+ "src/tools/expand-artifact.ts"() {
2044
+ "use strict";
2045
+ }
2046
+ });
2047
+
1579
2048
  // src/tools/executor.ts
1580
2049
  function normalizeToolOutput(result) {
1581
2050
  if (typeof result === "string") {
@@ -1599,6 +2068,9 @@ var init_executor = __esm({
1599
2068
  init_grep();
1600
2069
  init_web_fetch();
1601
2070
  init_tasks();
2071
+ init_artifact_store();
2072
+ init_reducer();
2073
+ init_expand_artifact();
1602
2074
  ALL_TOOLS = [
1603
2075
  readTool,
1604
2076
  writeTool,
@@ -1612,8 +2084,11 @@ var init_executor = __esm({
1612
2084
  ToolExecutor = class {
1613
2085
  sessionAllowed = /* @__PURE__ */ new Set();
1614
2086
  tools;
2087
+ artifactStore;
1615
2088
  constructor(tools = ALL_TOOLS) {
1616
2089
  this.tools = new Map(tools.map((t) => [t.name, t]));
2090
+ this.artifactStore = new ToolArtifactStore();
2091
+ this.tools.set("expand_artifact", makeExpandArtifactTool(this.artifactStore));
1617
2092
  }
1618
2093
  list() {
1619
2094
  return [...this.tools.values()];
@@ -1627,6 +2102,9 @@ var init_executor = __esm({
1627
2102
  clearSessionPermissions() {
1628
2103
  this.sessionAllowed.clear();
1629
2104
  }
2105
+ clearArtifacts() {
2106
+ this.artifactStore.clear();
2107
+ }
1630
2108
  async run(call, askPermission, ctx) {
1631
2109
  const tool = this.tools.get(call.name);
1632
2110
  if (!tool) {
@@ -1666,13 +2144,21 @@ var init_executor = __esm({
1666
2144
  try {
1667
2145
  const result = await tool.run(args, ctx);
1668
2146
  const normalized = normalizeToolOutput(result);
2147
+ const reduced = reduceToolOutput(
2148
+ call.name,
2149
+ normalized.content,
2150
+ args,
2151
+ this.artifactStore,
2152
+ DEFAULT_REDUCER_CONFIG
2153
+ );
1669
2154
  return {
1670
2155
  tool_call_id: call.id,
1671
2156
  name: call.name,
1672
- content: normalized.content,
2157
+ content: reduced.content,
1673
2158
  ok: true,
1674
- rawBytes: normalized.rawBytes,
1675
- reducedBytes: normalized.reducedBytes
2159
+ rawBytes: reduced.rawBytes,
2160
+ reducedBytes: reduced.reducedBytes,
2161
+ artifactId: reduced.artifactId
1676
2162
  };
1677
2163
  } catch (e) {
1678
2164
  const msg = `Error running ${call.name}: ${e.message ?? String(e)}`;
@@ -1700,16 +2186,16 @@ var init_executor = __esm({
1700
2186
  // src/util/update-check.ts
1701
2187
  import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
1702
2188
  import { homedir as homedir5 } from "os";
1703
- import { join as join5, dirname as dirname2 } from "path";
2189
+ import { join as join6, dirname as dirname2 } from "path";
1704
2190
  import { fileURLToPath } from "url";
1705
2191
  function cachePath() {
1706
- const xdg = process.env.XDG_CONFIG_HOME || join5(homedir5(), ".config");
1707
- return join5(xdg, "kimiflare", "update-check.json");
2192
+ const xdg = process.env.XDG_CONFIG_HOME || join6(homedir5(), ".config");
2193
+ return join6(xdg, "kimiflare", "update-check.json");
1708
2194
  }
1709
2195
  async function findPackageJson(startDir) {
1710
2196
  let dir = startDir;
1711
2197
  while (true) {
1712
- const candidate = join5(dir, "package.json");
2198
+ const candidate = join6(dir, "package.json");
1713
2199
  try {
1714
2200
  const raw = await readFile6(candidate, "utf8");
1715
2201
  const parsed = JSON.parse(raw);
@@ -1882,6 +2368,413 @@ Do not include speculation. Do not include chat-style pleasantries. Use short bu
1882
2368
  }
1883
2369
  });
1884
2370
 
2371
+ // src/agent/session-state.ts
2372
+ function emptySessionState(task = "") {
2373
+ return {
2374
+ task,
2375
+ user_constraints: [],
2376
+ repo_facts: [],
2377
+ files_touched: [],
2378
+ files_modified: [],
2379
+ confirmed_findings: [],
2380
+ open_questions: [],
2381
+ recent_failures: [],
2382
+ decisions: [],
2383
+ next_actions: [],
2384
+ artifact_index: {}
2385
+ };
2386
+ }
2387
+ function formatRecalledArtifacts(recalled) {
2388
+ if (recalled.length === 0) return "";
2389
+ const lines = ["[recalled artifacts]"];
2390
+ for (const { id, artifact } of recalled) {
2391
+ lines.push(`--- artifact:${id} (${artifact.type} from ${artifact.source}) ---`);
2392
+ lines.push(artifact.raw);
2393
+ }
2394
+ return lines.join("\n");
2395
+ }
2396
+ function serializeSessionState(state) {
2397
+ const lines = [];
2398
+ lines.push(`task: ${state.task || "(none)"}`);
2399
+ if (state.user_constraints.length) lines.push(`constraints:
2400
+ ${state.user_constraints.map((c) => " - " + c).join("\n")}`);
2401
+ if (state.repo_facts.length) lines.push(`repo_facts:
2402
+ ${state.repo_facts.map((f) => " - " + f).join("\n")}`);
2403
+ if (state.files_touched.length) lines.push(`files_touched: ${state.files_touched.join(", ")}`);
2404
+ if (state.files_modified.length) lines.push(`files_modified: ${state.files_modified.join(", ")}`);
2405
+ if (state.confirmed_findings.length) lines.push(`findings:
2406
+ ${state.confirmed_findings.map((f) => " - " + f).join("\n")}`);
2407
+ if (state.open_questions.length) lines.push(`open_questions:
2408
+ ${state.open_questions.map((q) => " - " + q).join("\n")}`);
2409
+ if (state.recent_failures.length) lines.push(`recent_failures:
2410
+ ${state.recent_failures.map((f) => " - " + f).join("\n")}`);
2411
+ if (state.decisions.length) lines.push(`decisions:
2412
+ ${state.decisions.map((d) => " - " + d).join("\n")}`);
2413
+ if (state.next_actions.length) lines.push(`next_actions:
2414
+ ${state.next_actions.map((a) => " - " + a).join("\n")}`);
2415
+ if (Object.keys(state.artifact_index).length) {
2416
+ lines.push("artifact_index:");
2417
+ for (const [id, meta] of Object.entries(state.artifact_index)) {
2418
+ lines.push(` ${id}: [${meta.type}] ${meta.summary}`);
2419
+ }
2420
+ }
2421
+ return lines.join("\n");
2422
+ }
2423
+ function buildSessionStateMessage(state) {
2424
+ return {
2425
+ role: "system",
2426
+ content: `[compiled session state]
2427
+ ${serializeSessionState(state)}`
2428
+ };
2429
+ }
2430
+ var ArtifactStore;
2431
+ var init_session_state = __esm({
2432
+ "src/agent/session-state.ts"() {
2433
+ "use strict";
2434
+ ArtifactStore = class {
2435
+ artifacts = /* @__PURE__ */ new Map();
2436
+ maxArtifacts;
2437
+ maxTotalChars;
2438
+ constructor(opts2) {
2439
+ this.maxArtifacts = opts2?.maxArtifacts ?? 200;
2440
+ this.maxTotalChars = opts2?.maxTotalChars ?? 5e5;
2441
+ }
2442
+ add(a) {
2443
+ while (this.totalChars() + a.raw.length > this.maxTotalChars && this.artifacts.size > 0) {
2444
+ this.evictOldest();
2445
+ }
2446
+ while (this.artifacts.size >= this.maxArtifacts) {
2447
+ this.evictOldest();
2448
+ }
2449
+ this.artifacts.set(a.id, a);
2450
+ }
2451
+ get(id) {
2452
+ return this.artifacts.get(id);
2453
+ }
2454
+ has(id) {
2455
+ return this.artifacts.has(id);
2456
+ }
2457
+ list() {
2458
+ return [...this.artifacts.values()].sort((a, b) => a.ts < b.ts ? -1 : 1);
2459
+ }
2460
+ recall(ids) {
2461
+ const out = [];
2462
+ for (const id of ids) {
2463
+ const a = this.artifacts.get(id);
2464
+ if (a) out.push({ id, artifact: a });
2465
+ }
2466
+ return out;
2467
+ }
2468
+ size() {
2469
+ return this.artifacts.size;
2470
+ }
2471
+ totalChars() {
2472
+ let sum = 0;
2473
+ for (const a of this.artifacts.values()) {
2474
+ sum += a.raw.length;
2475
+ }
2476
+ return sum;
2477
+ }
2478
+ evictOldest() {
2479
+ let oldest;
2480
+ for (const a of this.artifacts.values()) {
2481
+ if (!oldest || a.ts < oldest.ts) oldest = a;
2482
+ }
2483
+ if (oldest) this.artifacts.delete(oldest.id);
2484
+ }
2485
+ };
2486
+ }
2487
+ });
2488
+
2489
+ // src/agent/compaction.ts
2490
+ function approxTokens2(n) {
2491
+ return Math.round(n / 4);
2492
+ }
2493
+ function estimateMessageTokens(m) {
2494
+ let chars = 0;
2495
+ if (typeof m.content === "string") {
2496
+ chars = m.content.length;
2497
+ } else if (Array.isArray(m.content)) {
2498
+ chars = m.content.map((p) => p.type === "text" ? p.text.length : 0).reduce((a, b) => a + b, 0);
2499
+ }
2500
+ if (m.reasoning_content) chars += m.reasoning_content.length;
2501
+ if (m.tool_calls) {
2502
+ for (const tc of m.tool_calls) {
2503
+ chars += tc.function.name.length + tc.function.arguments.length;
2504
+ }
2505
+ }
2506
+ return approxTokens2(chars);
2507
+ }
2508
+ function estimatePromptTokens(messages) {
2509
+ return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
2510
+ }
2511
+ function groupIntoTurns(messages) {
2512
+ const prefix = [];
2513
+ let i = 0;
2514
+ while (i < messages.length && messages[i].role === "system") {
2515
+ prefix.push(messages[i]);
2516
+ i++;
2517
+ }
2518
+ const turns = [];
2519
+ while (i < messages.length) {
2520
+ if (messages[i].role !== "user") {
2521
+ i++;
2522
+ continue;
2523
+ }
2524
+ const user = messages[i];
2525
+ i++;
2526
+ if (i >= messages.length || messages[i].role !== "assistant") {
2527
+ continue;
2528
+ }
2529
+ const assistant = messages[i];
2530
+ i++;
2531
+ const tools = [];
2532
+ while (i < messages.length && messages[i].role === "tool") {
2533
+ tools.push(messages[i]);
2534
+ i++;
2535
+ }
2536
+ turns.push({ user, assistant, tools });
2537
+ }
2538
+ return { prefix, turns };
2539
+ }
2540
+ function makeArtifactId(type, index) {
2541
+ return `${type}_${Date.now()}_${index}`;
2542
+ }
2543
+ function extractArtifactsFromTurn(turn, startIndex, store) {
2544
+ const artifacts = [];
2545
+ const stateDelta = {
2546
+ files_touched: [],
2547
+ files_modified: [],
2548
+ confirmed_findings: [],
2549
+ recent_failures: [],
2550
+ decisions: [],
2551
+ next_actions: []
2552
+ };
2553
+ const toolCalls = turn.assistant.tool_calls ?? [];
2554
+ for (let ti = 0; ti < turn.tools.length; ti++) {
2555
+ const tm = turn.tools[ti];
2556
+ const tc = toolCalls[ti];
2557
+ const name = tm.name ?? tc?.function.name ?? "unknown";
2558
+ const content = typeof tm.content === "string" ? tm.content : "";
2559
+ let type = "tool_result";
2560
+ let summary = `${name} result`;
2561
+ let path;
2562
+ if (name === "read") {
2563
+ type = "read_slice";
2564
+ try {
2565
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2566
+ path = args.path;
2567
+ summary = `read ${path ?? "file"}`;
2568
+ if (path && !stateDelta.files_touched.includes(path)) stateDelta.files_touched.push(path);
2569
+ } catch {
2570
+ summary = "read file";
2571
+ }
2572
+ } else if (name === "bash") {
2573
+ type = "bash_log";
2574
+ try {
2575
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2576
+ const cmd = args.command ?? "";
2577
+ summary = `bash: ${cmd.slice(0, 60)}`;
2578
+ if (content.includes("Error") || content.includes("error") || content.includes("FAIL")) {
2579
+ stateDelta.recent_failures.push(`bash failed: ${cmd.slice(0, 80)}`);
2580
+ }
2581
+ } catch {
2582
+ summary = "bash command";
2583
+ }
2584
+ } else if (name === "grep") {
2585
+ type = "grep_result";
2586
+ summary = `grep results (${content.split("\n").length} lines)`;
2587
+ } else if (name === "web_fetch") {
2588
+ type = "web_fetch";
2589
+ try {
2590
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2591
+ summary = `web_fetch: ${args.url ?? "url"}`;
2592
+ } catch {
2593
+ summary = "web_fetch";
2594
+ }
2595
+ } else if (name === "write" || name === "edit") {
2596
+ try {
2597
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2598
+ path = args.path;
2599
+ if (path && !stateDelta.files_modified.includes(path)) stateDelta.files_modified.push(path);
2600
+ if (path && !stateDelta.files_touched.includes(path)) stateDelta.files_touched.push(path);
2601
+ } catch {
2602
+ }
2603
+ continue;
2604
+ } else if (name === "glob") {
2605
+ try {
2606
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2607
+ summary = `glob: ${args.pattern ?? ""}`;
2608
+ } catch {
2609
+ summary = "glob";
2610
+ }
2611
+ } else if (name === "tasks_set") {
2612
+ try {
2613
+ const args = tc ? JSON.parse(tc.function.arguments) : {};
2614
+ const tasks = args.tasks ?? [];
2615
+ const inProgress = tasks.filter((t) => t.status === "in_progress").map((t) => t.title);
2616
+ const pending = tasks.filter((t) => t.status === "pending").map((t) => t.title);
2617
+ if (inProgress.length) stateDelta.next_actions.push(...inProgress);
2618
+ if (pending.length) stateDelta.next_actions.push(...pending);
2619
+ summary = `tasks_set: ${tasks.length} tasks`;
2620
+ } catch {
2621
+ summary = "tasks_set";
2622
+ }
2623
+ }
2624
+ const maxRaw = 5e4;
2625
+ const raw = content.length > maxRaw ? content.slice(0, maxRaw) + `
2626
+ ...[${content.length - maxRaw} chars truncated]` : content;
2627
+ const artifact = {
2628
+ id: makeArtifactId(type, startIndex + ti),
2629
+ type,
2630
+ summary,
2631
+ raw,
2632
+ source: name,
2633
+ path,
2634
+ ts: (/* @__PURE__ */ new Date()).toISOString()
2635
+ };
2636
+ artifacts.push(artifact);
2637
+ if (!content.includes("Error") && !content.includes("error") && content.length > 0 && content.length < 2e3) {
2638
+ stateDelta.confirmed_findings.push(`${name}: ${content.slice(0, 200)}`);
2639
+ }
2640
+ }
2641
+ const assistantText = typeof turn.assistant.content === "string" ? turn.assistant.content : "";
2642
+ if (assistantText.length > 0) {
2643
+ const decisionPatterns = [
2644
+ /(?:decided?|will|plan to|going to|should|need to)\s+(.{10,200})/gi,
2645
+ /(?:let's|let us)\s+(.{10,200})/gi
2646
+ ];
2647
+ for (const pattern of decisionPatterns) {
2648
+ let match;
2649
+ while ((match = pattern.exec(assistantText)) !== null) {
2650
+ const decision = match[1].trim().replace(/\.$/, "");
2651
+ if (decision.length > 10 && !stateDelta.decisions.includes(decision)) {
2652
+ stateDelta.decisions.push(decision);
2653
+ }
2654
+ }
2655
+ }
2656
+ }
2657
+ return { artifacts, stateDelta };
2658
+ }
2659
+ function mergeState(state, delta) {
2660
+ const mergeArr = (a, b) => {
2661
+ if (!b) return a;
2662
+ const set = new Set(a);
2663
+ for (const item of b) set.add(item);
2664
+ return [...set];
2665
+ };
2666
+ return {
2667
+ ...state,
2668
+ task: state.task || delta.task || "",
2669
+ user_constraints: mergeArr(state.user_constraints, delta.user_constraints),
2670
+ repo_facts: mergeArr(state.repo_facts, delta.repo_facts),
2671
+ files_touched: mergeArr(state.files_touched, delta.files_touched),
2672
+ files_modified: mergeArr(state.files_modified, delta.files_modified),
2673
+ confirmed_findings: mergeArr(state.confirmed_findings, delta.confirmed_findings),
2674
+ open_questions: mergeArr(state.open_questions, delta.open_questions),
2675
+ recent_failures: mergeArr(state.recent_failures, delta.recent_failures),
2676
+ decisions: mergeArr(state.decisions, delta.decisions),
2677
+ next_actions: mergeArr(state.next_actions, delta.next_actions),
2678
+ artifact_index: { ...state.artifact_index, ...delta.artifact_index }
2679
+ };
2680
+ }
2681
+ function shouldCompact(opts2) {
2682
+ const tokenThreshold = opts2.tokenThreshold ?? 8e4;
2683
+ const turnThreshold = opts2.turnThreshold ?? 12;
2684
+ const tokens = estimatePromptTokens(opts2.messages);
2685
+ const { turns } = groupIntoTurns(opts2.messages);
2686
+ return tokens > tokenThreshold || turns.length > turnThreshold;
2687
+ }
2688
+ function compactMessages2(opts2) {
2689
+ const keepLastTurns = opts2.keepLastTurns ?? 4;
2690
+ const { prefix, turns } = groupIntoTurns(opts2.messages);
2691
+ const tokensBefore = estimatePromptTokens(opts2.messages);
2692
+ if (turns.length <= keepLastTurns) {
2693
+ return {
2694
+ newMessages: opts2.messages,
2695
+ newState: opts2.state,
2696
+ metrics: {
2697
+ estimatedTokensBefore: tokensBefore,
2698
+ estimatedTokensAfter: tokensBefore,
2699
+ archivedArtifacts: 0,
2700
+ recalledArtifacts: 0,
2701
+ rawTurnsRemoved: 0,
2702
+ rawTurnsKept: turns.length
2703
+ }
2704
+ };
2705
+ }
2706
+ const toCompact = turns.slice(0, turns.length - keepLastTurns);
2707
+ const toKeep = turns.slice(turns.length - keepLastTurns);
2708
+ let newState = { ...opts2.state };
2709
+ let archivedCount = 0;
2710
+ for (let i = 0; i < toCompact.length; i++) {
2711
+ const turn = toCompact[i];
2712
+ const { artifacts, stateDelta } = extractArtifactsFromTurn(turn, i, opts2.store);
2713
+ for (const artifact of artifacts) {
2714
+ opts2.store.add(artifact);
2715
+ archivedCount++;
2716
+ newState.artifact_index[artifact.id] = {
2717
+ type: artifact.type,
2718
+ summary: artifact.summary,
2719
+ source: artifact.source,
2720
+ path: artifact.path
2721
+ };
2722
+ }
2723
+ newState = mergeState(newState, stateDelta);
2724
+ if (!newState.task && typeof turn.user.content === "string") {
2725
+ newState.task = turn.user.content.slice(0, 200);
2726
+ }
2727
+ }
2728
+ const workingMemory = [];
2729
+ for (const turn of toKeep) {
2730
+ workingMemory.push(turn.user);
2731
+ workingMemory.push(turn.assistant);
2732
+ for (const tm of turn.tools) {
2733
+ workingMemory.push(tm);
2734
+ }
2735
+ }
2736
+ const stateMsg = buildSessionStateMessage(newState);
2737
+ const newMessages = [...prefix, stateMsg, ...workingMemory];
2738
+ const tokensAfter = estimatePromptTokens(newMessages);
2739
+ const metrics = {
2740
+ estimatedTokensBefore: tokensBefore,
2741
+ estimatedTokensAfter: tokensAfter,
2742
+ archivedArtifacts: archivedCount,
2743
+ recalledArtifacts: 0,
2744
+ rawTurnsRemoved: toCompact.length,
2745
+ rawTurnsKept: toKeep.length
2746
+ };
2747
+ return { newMessages, newState, metrics };
2748
+ }
2749
+ function recallArtifacts(messages, store, state) {
2750
+ const text = messages.map((m) => typeof m.content === "string" ? m.content : "").join(" ");
2751
+ const ids = [];
2752
+ for (const [id, meta] of Object.entries(state.artifact_index)) {
2753
+ if (meta.path && text.includes(meta.path)) {
2754
+ ids.push(id);
2755
+ }
2756
+ }
2757
+ for (const failure of state.recent_failures) {
2758
+ const keyword = failure.split(":")[0];
2759
+ if (keyword && text.toLowerCase().includes(keyword.toLowerCase())) {
2760
+ for (const [id, meta] of Object.entries(state.artifact_index)) {
2761
+ if (meta.source === "bash" && !ids.includes(id)) {
2762
+ ids.push(id);
2763
+ }
2764
+ }
2765
+ }
2766
+ }
2767
+ const uniqueIds = [...new Set(ids)].slice(0, 5);
2768
+ const recalled = store.recall(uniqueIds);
2769
+ return { ids: uniqueIds, recalled };
2770
+ }
2771
+ var init_compaction = __esm({
2772
+ "src/agent/compaction.ts"() {
2773
+ "use strict";
2774
+ init_session_state();
2775
+ }
2776
+ });
2777
+
1885
2778
  // src/mcp/adapter.ts
1886
2779
  function mcpToolToSpec(serverName, mcpTool, client) {
1887
2780
  const prefix = `mcp_${sanitizeName(serverName)}_`;
@@ -2416,7 +3309,7 @@ function buildRightParts(usage, contextLimit) {
2416
3309
  `in ${usage.prompt_tokens}${cached ? ` (${cached} cached)` : ""}`,
2417
3310
  `out ${usage.completion_tokens}`,
2418
3311
  `ctx ${pct}%`,
2419
- `${cost.total.toFixed(5)}`
3312
+ `$${cost.total.toFixed(5)}`
2420
3313
  ];
2421
3314
  }
2422
3315
  function shortModel(m) {
@@ -3834,12 +4727,20 @@ var init_theme = __esm({
3834
4727
  });
3835
4728
 
3836
4729
  // src/sessions.ts
3837
- import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5, readdir, stat as stat2 } from "fs/promises";
4730
+ var sessions_exports = {};
4731
+ __export(sessions_exports, {
4732
+ listSessions: () => listSessions,
4733
+ loadSession: () => loadSession,
4734
+ makeSessionId: () => makeSessionId,
4735
+ pruneSessions: () => pruneSessions,
4736
+ saveSession: () => saveSession
4737
+ });
4738
+ import { readFile as readFile7, writeFile as writeFile5, mkdir as mkdir5, readdir as readdir2, stat as stat3 } from "fs/promises";
3838
4739
  import { homedir as homedir6 } from "os";
3839
- import { join as join6 } from "path";
4740
+ import { join as join7 } from "path";
3840
4741
  function sessionsDir() {
3841
- const xdg = process.env.XDG_DATA_HOME || join6(homedir6(), ".local", "share");
3842
- return join6(xdg, "kimiflare", "sessions");
4742
+ const xdg = process.env.XDG_DATA_HOME || join7(homedir6(), ".local", "share");
4743
+ return join7(xdg, "kimiflare", "sessions");
3843
4744
  }
3844
4745
  function sanitize(text) {
3845
4746
  return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
@@ -3852,24 +4753,29 @@ function makeSessionId(firstPrompt) {
3852
4753
  async function saveSession(file) {
3853
4754
  const dir = sessionsDir();
3854
4755
  await mkdir5(dir, { recursive: true });
3855
- const path = join6(dir, `${file.id}.json`);
4756
+ const path = join7(dir, `${file.id}.json`);
3856
4757
  await writeFile5(path, JSON.stringify(file, null, 2), "utf8");
3857
4758
  return path;
3858
4759
  }
4760
+ async function pruneSessions() {
4761
+ const dir = sessionsDir();
4762
+ const files = await listFilesByMtime(dir, /\.json$/);
4763
+ return pruneFiles(files, RETENTION.sessionMaxAgeDays, RETENTION.sessionMaxCount);
4764
+ }
3859
4765
  async function listSessions(limit = 30) {
3860
4766
  const dir = sessionsDir();
3861
4767
  let entries;
3862
4768
  try {
3863
- entries = await readdir(dir);
4769
+ entries = await readdir2(dir);
3864
4770
  } catch {
3865
4771
  return [];
3866
4772
  }
3867
4773
  const summaries = [];
3868
4774
  for (const name of entries) {
3869
4775
  if (!name.endsWith(".json")) continue;
3870
- const path = join6(dir, name);
4776
+ const path = join7(dir, name);
3871
4777
  try {
3872
- const [s, raw] = await Promise.all([stat2(path), readFile7(path, "utf8")]);
4778
+ const [s, raw] = await Promise.all([stat3(path), readFile7(path, "utf8")]);
3873
4779
  const parsed = JSON.parse(raw);
3874
4780
  const firstUser = parsed.messages.find((m) => m.role === "user");
3875
4781
  const firstPrompt = typeof firstUser?.content === "string" ? firstUser.content : firstUser?.content ? firstUser.content.find((p) => p.type === "text")?.text ?? "(no prompt)" : "(no prompt)";
@@ -3894,6 +4800,7 @@ async function loadSession(filePath) {
3894
4800
  var init_sessions = __esm({
3895
4801
  "src/sessions.ts"() {
3896
4802
  "use strict";
4803
+ init_storage_limits();
3897
4804
  }
3898
4805
  });
3899
4806
 
@@ -3939,17 +4846,21 @@ var init_image = __esm({
3939
4846
  // src/usage-tracker.ts
3940
4847
  import { readFile as readFile9, writeFile as writeFile6, mkdir as mkdir6 } from "fs/promises";
3941
4848
  import { homedir as homedir7 } from "os";
3942
- import { join as join7 } from "path";
4849
+ import { join as join8 } from "path";
3943
4850
  function usageDir() {
3944
- const xdg = process.env.XDG_DATA_HOME || join7(homedir7(), ".local", "share");
3945
- return join7(xdg, "kimiflare");
4851
+ const xdg = process.env.XDG_DATA_HOME || join8(homedir7(), ".local", "share");
4852
+ return join8(xdg, "kimiflare");
3946
4853
  }
3947
4854
  function usagePath() {
3948
- return join7(usageDir(), "usage.json");
4855
+ return join8(usageDir(), "usage.json");
3949
4856
  }
3950
4857
  function today() {
3951
4858
  return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3952
4859
  }
4860
+ function cutoffDate(daysBack) {
4861
+ const d = new Date(Date.now() - daysBack * 24 * 60 * 60 * 1e3);
4862
+ return d.toISOString().slice(0, 10);
4863
+ }
3953
4864
  async function loadLog() {
3954
4865
  try {
3955
4866
  const raw = await readFile9(usagePath(), "utf8");
@@ -3979,8 +4890,18 @@ function getOrCreateSession(log, sessionId, date) {
3979
4890
  }
3980
4891
  return session;
3981
4892
  }
4893
+ function pruneUsageLog(log) {
4894
+ const dayCutoff = cutoffDate(RETENTION.usageDayMaxAgeDays);
4895
+ const sessionCutoff = cutoffDate(RETENTION.usageSessionMaxAgeDays);
4896
+ const days = log.days.filter((d) => d.date >= dayCutoff);
4897
+ let sessions = log.sessions.filter((s) => s.date >= sessionCutoff);
4898
+ if (sessions.length > RETENTION.usageSessionMaxCount) {
4899
+ sessions = sessions.sort((a, b) => b.date < a.date ? -1 : b.date > a.date ? 1 : 0).slice(0, RETENTION.usageSessionMaxCount);
4900
+ }
4901
+ return { ...log, days, sessions };
4902
+ }
3982
4903
  async function recordUsage(sessionId, usage) {
3983
- const log = await loadLog();
4904
+ const log = pruneUsageLog(await loadLog());
3984
4905
  const date = today();
3985
4906
  const cost = calculateCost(usage.prompt_tokens, usage.completion_tokens, usage.prompt_tokens_details?.cached_tokens ?? 0);
3986
4907
  const day = getOrCreateDay(log, date);
@@ -3996,7 +4917,7 @@ async function recordUsage(sessionId, usage) {
3996
4917
  await saveLog(log);
3997
4918
  }
3998
4919
  async function getCostReport(sessionId) {
3999
- const log = await loadLog();
4920
+ const log = pruneUsageLog(await loadLog());
4000
4921
  const date = today();
4001
4922
  const currentMonth = date.slice(0, 7);
4002
4923
  const session = log.sessions.find((s) => s.id === sessionId) ?? { date, promptTokens: 0, completionTokens: 0, cachedTokens: 0, cost: 0 };
@@ -4055,6 +4976,7 @@ var init_usage_tracker = __esm({
4055
4976
  "src/usage-tracker.ts"() {
4056
4977
  "use strict";
4057
4978
  init_pricing();
4979
+ init_storage_limits();
4058
4980
  LOG_VERSION2 = 1;
4059
4981
  }
4060
4982
  });
@@ -4067,8 +4989,8 @@ __export(app_exports, {
4067
4989
  import { useState as useState6, useRef as useRef3, useEffect as useEffect4, useCallback } from "react";
4068
4990
  import { Box as Box12, Text as Text13, useApp, useInput as useInput2, render } from "ink";
4069
4991
  import { existsSync } from "fs";
4070
- import { join as join8 } from "path";
4071
- import { unlink } from "fs/promises";
4992
+ import { join as join9 } from "path";
4993
+ import { unlink as unlink2 } from "fs/promises";
4072
4994
  import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
4073
4995
  function capEvents(prev) {
4074
4996
  if (prev.length <= MAX_EVENTS) return prev;
@@ -4145,11 +5067,27 @@ function App({ initialCfg, initialUpdateResult }) {
4145
5067
  const tasksRef = useRef3([]);
4146
5068
  const usageRef = useRef3(null);
4147
5069
  const updateCheckedRef = useRef3(false);
5070
+ const sessionStateRef = useRef3(emptySessionState());
5071
+ const artifactStoreRef = useRef3(new ArtifactStore());
5072
+ const compiledContextRef = useRef3(initialCfg?.compiledContext === true);
4148
5073
  const updateNudgedRef = useRef3(false);
4149
5074
  const compactSuggestedRef = useRef3(false);
4150
5075
  const mcpManagerRef = useRef3(new McpManager());
4151
5076
  const mcpToolsRef = useRef3([]);
4152
5077
  const mcpInitRef = useRef3(false);
5078
+ useEffect4(() => {
5079
+ if (!cfg) return;
5080
+ void Promise.resolve().then(() => (init_sessions(), sessions_exports)).then(
5081
+ ({ pruneSessions: pruneSessions2 }) => pruneSessions2().then((removed) => {
5082
+ if (removed > 0) {
5083
+ setEvents((e) => [
5084
+ ...e,
5085
+ { kind: "info", key: mkKey(), text: `pruned ${removed} old session files` }
5086
+ ]);
5087
+ }
5088
+ })
5089
+ );
5090
+ }, [cfg]);
4153
5091
  useEffect4(() => {
4154
5092
  if (!cfg || updateCheckedRef.current) return;
4155
5093
  updateCheckedRef.current = true;
@@ -4351,7 +5289,8 @@ function App({ initialCfg, initialUpdateResult }) {
4351
5289
  model: cfg.model,
4352
5290
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
4353
5291
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4354
- messages: messagesRef.current
5292
+ messages: messagesRef.current,
5293
+ sessionState: compiledContextRef.current ? sessionStateRef.current : void 0
4355
5294
  });
4356
5295
  } catch {
4357
5296
  }
@@ -4416,29 +5355,55 @@ function App({ initialCfg, initialUpdateResult }) {
4416
5355
  const controller = new AbortController();
4417
5356
  activeControllerRef.current = controller;
4418
5357
  try {
4419
- const result = await compactMessages({
4420
- accountId: cfg.accountId,
4421
- apiToken: cfg.apiToken,
4422
- model: cfg.model,
4423
- messages: messagesRef.current,
4424
- signal: controller.signal
4425
- });
4426
- if (result.replacedCount === 0) {
4427
- setEvents((e) => [
4428
- ...e,
4429
- { kind: "info", key: mkKey(), text: "nothing to compact yet" }
4430
- ]);
5358
+ if (compiledContextRef.current) {
5359
+ const result = compactMessages2({
5360
+ messages: messagesRef.current,
5361
+ state: sessionStateRef.current,
5362
+ store: artifactStoreRef.current
5363
+ });
5364
+ if (result.metrics.rawTurnsRemoved === 0) {
5365
+ setEvents((e) => [
5366
+ ...e,
5367
+ { kind: "info", key: mkKey(), text: "nothing to compact yet" }
5368
+ ]);
5369
+ } else {
5370
+ messagesRef.current = result.newMessages;
5371
+ sessionStateRef.current = result.newState;
5372
+ setEvents((e) => [
5373
+ ...e,
5374
+ {
5375
+ kind: "info",
5376
+ key: mkKey(),
5377
+ text: `compacted ${result.metrics.rawTurnsRemoved} turns \u2192 ${result.metrics.estimatedTokensBefore} \u2192 ${result.metrics.estimatedTokensAfter} tokens, ${result.metrics.archivedArtifacts} artifacts`
5378
+ }
5379
+ ]);
5380
+ await saveSessionSafe();
5381
+ }
4431
5382
  } else {
4432
- messagesRef.current = result.newMessages;
4433
- setEvents((e) => [
4434
- ...e,
4435
- {
4436
- kind: "info",
4437
- key: mkKey(),
4438
- text: `compacted ${result.replacedCount} messages into a summary`
4439
- }
4440
- ]);
4441
- await saveSessionSafe();
5383
+ const result = await compactMessages({
5384
+ accountId: cfg.accountId,
5385
+ apiToken: cfg.apiToken,
5386
+ model: cfg.model,
5387
+ messages: messagesRef.current,
5388
+ signal: controller.signal
5389
+ });
5390
+ if (result.replacedCount === 0) {
5391
+ setEvents((e) => [
5392
+ ...e,
5393
+ { kind: "info", key: mkKey(), text: "nothing to compact yet" }
5394
+ ]);
5395
+ } else {
5396
+ messagesRef.current = result.newMessages;
5397
+ setEvents((e) => [
5398
+ ...e,
5399
+ {
5400
+ kind: "info",
5401
+ key: mkKey(),
5402
+ text: `compacted ${result.replacedCount} messages into a summary`
5403
+ }
5404
+ ]);
5405
+ await saveSessionSafe();
5406
+ }
4442
5407
  }
4443
5408
  } catch (e) {
4444
5409
  if (e.name !== "AbortError") {
@@ -4465,13 +5430,13 @@ function App({ initialCfg, initialUpdateResult }) {
4465
5430
  }
4466
5431
  const cwd = process.cwd();
4467
5432
  for (const name of ["KIMI.md", "KIMIFLARE.md", "AGENT.md"]) {
4468
- if (existsSync(join8(cwd, name))) {
5433
+ if (existsSync(join9(cwd, name))) {
4469
5434
  setEvents((e) => [
4470
5435
  ...e,
4471
5436
  {
4472
5437
  kind: "info",
4473
5438
  key: mkKey(),
4474
- text: `${name} already exists at ${join8(cwd, name)} \u2014 delete it first if you want to regenerate`
5439
+ text: `${name} already exists at ${join9(cwd, name)} \u2014 delete it first if you want to regenerate`
4475
5440
  }
4476
5441
  ]);
4477
5442
  return;
@@ -4588,7 +5553,7 @@ function App({ initialCfg, initialUpdateResult }) {
4588
5553
  })
4589
5554
  }
4590
5555
  });
4591
- if (existsSync(join8(cwd, "KIMI.md"))) {
5556
+ if (existsSync(join9(cwd, "KIMI.md"))) {
4592
5557
  if (cacheStableRef.current) {
4593
5558
  messagesRef.current[1] = {
4594
5559
  role: "system",
@@ -4637,6 +5602,10 @@ function App({ initialCfg, initialUpdateResult }) {
4637
5602
  const file = await loadSession(picked.filePath);
4638
5603
  messagesRef.current = file.messages;
4639
5604
  sessionIdRef.current = file.id;
5605
+ if (file.sessionState && compiledContextRef.current) {
5606
+ sessionStateRef.current = file.sessionState;
5607
+ artifactStoreRef.current = new ArtifactStore();
5608
+ }
4640
5609
  setEvents([
4641
5610
  {
4642
5611
  kind: "info",
@@ -4699,6 +5668,9 @@ function App({ initialCfg, initialUpdateResult }) {
4699
5668
  messagesRef.current = [messagesRef.current[0]];
4700
5669
  }
4701
5670
  sessionIdRef.current = null;
5671
+ sessionStateRef.current = emptySessionState();
5672
+ artifactStoreRef.current = new ArtifactStore();
5673
+ executorRef.current.clearArtifacts();
4702
5674
  setEvents([]);
4703
5675
  setUsage(null);
4704
5676
  setTasks([]);
@@ -4926,7 +5898,7 @@ use: /thinking low | medium | high`
4926
5898
  return true;
4927
5899
  }
4928
5900
  if (c === "/logout") {
4929
- unlink(configPath()).catch(() => {
5901
+ unlink2(configPath()).catch(() => {
4930
5902
  });
4931
5903
  setEvents((e) => [
4932
5904
  ...e,
@@ -4987,6 +5959,17 @@ use: /thinking low | medium | high`
4987
5959
  }
4988
5960
  setEvents((e) => [...e, { kind: "user", key: mkKey(), text: display, images: images.length > 0 ? images : void 0 }]);
4989
5961
  messagesRef.current.push({ role: "user", content });
5962
+ if (compiledContextRef.current) {
5963
+ const { ids, recalled } = recallArtifacts(messagesRef.current, artifactStoreRef.current, sessionStateRef.current);
5964
+ if (recalled.length > 0) {
5965
+ const recalledText = formatRecalledArtifacts(recalled);
5966
+ messagesRef.current.push({ role: "system", content: recalledText });
5967
+ sessionStateRef.current = {
5968
+ ...sessionStateRef.current,
5969
+ artifact_index: { ...sessionStateRef.current.artifact_index }
5970
+ };
5971
+ }
5972
+ }
4990
5973
  setBusy(true);
4991
5974
  setTurnStartedAt(Date.now());
4992
5975
  const controller = new AbortController();
@@ -5096,6 +6079,26 @@ use: /thinking low | medium | high`
5096
6079
  }
5097
6080
  });
5098
6081
  await saveSessionSafe();
6082
+ if (compiledContextRef.current && shouldCompact({ messages: messagesRef.current })) {
6083
+ const result = compactMessages2({
6084
+ messages: messagesRef.current,
6085
+ state: sessionStateRef.current,
6086
+ store: artifactStoreRef.current
6087
+ });
6088
+ if (result.metrics.rawTurnsRemoved > 0) {
6089
+ messagesRef.current = result.newMessages;
6090
+ sessionStateRef.current = result.newState;
6091
+ setEvents((e) => [
6092
+ ...e,
6093
+ {
6094
+ kind: "info",
6095
+ key: mkKey(),
6096
+ text: `auto-compacted: ${result.metrics.estimatedTokensBefore} \u2192 ${result.metrics.estimatedTokensAfter} tokens (${result.metrics.archivedArtifacts} artifacts)`
6097
+ }
6098
+ ]);
6099
+ await saveSessionSafe();
6100
+ }
6101
+ }
5099
6102
  } catch (e) {
5100
6103
  if (e.name === "AbortError") {
5101
6104
  setEvents((es) => [...es, { kind: "info", key: mkKey(), text: "(aborted)" }]);
@@ -5292,6 +6295,8 @@ var init_app = __esm({
5292
6295
  init_loop();
5293
6296
  init_system_prompt();
5294
6297
  init_compact();
6298
+ init_compaction();
6299
+ init_session_state();
5295
6300
  init_executor();
5296
6301
  init_manager();
5297
6302
  init_messages();
@@ -5336,11 +6341,11 @@ init_update_check();
5336
6341
  import { Command } from "commander";
5337
6342
  import { readFileSync as readFileSync2 } from "fs";
5338
6343
  import { fileURLToPath as fileURLToPath2 } from "url";
5339
- import { dirname as dirname3, join as join9 } from "path";
6344
+ import { dirname as dirname3, join as join10 } from "path";
5340
6345
  function readPackageVersion() {
5341
6346
  try {
5342
6347
  const here = dirname3(fileURLToPath2(import.meta.url));
5343
- const pkg = JSON.parse(readFileSync2(join9(here, "..", "package.json"), "utf8"));
6348
+ const pkg = JSON.parse(readFileSync2(join10(here, "..", "package.json"), "utf8"));
5344
6349
  return pkg.version ?? "0.0.0";
5345
6350
  } catch {
5346
6351
  return "0.0.0";