@mindstudio-ai/remy 0.1.194 → 0.1.196

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/headless.js CHANGED
@@ -647,375 +647,215 @@ async function generateBackgroundAck(params) {
647
647
  }
648
648
  }
649
649
 
650
- // src/usageLedger.ts
651
- import fs5 from "fs";
652
- var LEDGER_FILE = ".logs/usage.ndjson";
653
- var fd = null;
654
- function nanoToDollars(nano) {
655
- return typeof nano === "number" ? nano / 1e9 : void 0;
656
- }
657
- function recordUsage(entry) {
658
- try {
659
- if (fd === null) {
660
- fs5.mkdirSync(".logs", { recursive: true });
661
- fd = fs5.openSync(LEDGER_FILE, "a");
650
+ // src/tools/spec/readSpec.ts
651
+ import fs5 from "fs/promises";
652
+
653
+ // src/tools/spec/_helpers.ts
654
+ var HEADING_RE = /^(#{1,6})\s+(.+)$/;
655
+ function parseHeadings(content) {
656
+ const lines = content.split("\n");
657
+ const headings = [];
658
+ for (let i = 0; i < lines.length; i++) {
659
+ const match = lines[i].match(HEADING_RE);
660
+ if (match) {
661
+ headings.push({
662
+ level: match[1].length,
663
+ text: match[2].trim(),
664
+ startLine: i,
665
+ contentStart: i + 1,
666
+ contentEnd: lines.length
667
+ // placeholder — resolved below
668
+ });
662
669
  }
663
- fs5.writeSync(fd, JSON.stringify(entry) + "\n");
664
- } catch {
665
670
  }
671
+ for (let i = 0; i < headings.length; i++) {
672
+ const current = headings[i];
673
+ let end = lines.length;
674
+ for (let j = i + 1; j < headings.length; j++) {
675
+ if (headings[j].level <= current.level) {
676
+ end = headings[j].startLine;
677
+ break;
678
+ }
679
+ }
680
+ current.contentEnd = end;
681
+ }
682
+ return headings;
666
683
  }
667
-
668
- // src/compaction/index.ts
669
- var log3 = createLogger("compaction");
670
- var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
671
- var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
672
- var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
673
- async function compactConversation(messages, apiConfig, system, tools2, model) {
674
- const endIndex = findSafeInsertionPoint(messages);
675
- const summaries = [];
676
- const tasks = [];
677
- const conversationMessages = getConversationMessagesForSummary(
678
- messages,
679
- endIndex
680
- );
681
- if (conversationMessages.length > 0) {
682
- tasks.push(
683
- generateSummary(
684
- apiConfig,
685
- "conversation",
686
- CONVERSATION_SUMMARY_PROMPT,
687
- conversationMessages,
688
- system,
689
- tools2,
690
- model
691
- ).then((text) => {
692
- if (text) {
693
- summaries.push({ name: "conversation", text });
694
- }
695
- })
696
- );
684
+ function resolveHeadingPath(content, headingPath) {
685
+ const lines = content.split("\n");
686
+ const headings = parseHeadings(content);
687
+ if (headingPath === "") {
688
+ const firstHeadingLine = headings.length > 0 ? headings[0].startLine : lines.length;
689
+ return {
690
+ startLine: 0,
691
+ contentStart: 0,
692
+ contentEnd: firstHeadingLine
693
+ };
697
694
  }
698
- for (const name of SUMMARIZABLE_SUBAGENTS) {
699
- const subagentMessages = getSubAgentMessagesForSummary(
700
- messages,
701
- name,
702
- endIndex
695
+ const segments = headingPath.split(">").map((s) => s.trim());
696
+ let searchStart = 0;
697
+ let searchEnd = lines.length;
698
+ let resolved = null;
699
+ for (let si = 0; si < segments.length; si++) {
700
+ const segment = segments[si].toLowerCase();
701
+ const candidates = headings.filter(
702
+ (h) => h.startLine >= searchStart && h.startLine < searchEnd && h.text.toLowerCase() === segment
703
703
  );
704
- if (subagentMessages.length > 0) {
705
- tasks.push(
706
- generateSummary(
707
- apiConfig,
708
- name,
709
- SUBAGENT_SUMMARY_PROMPT,
710
- subagentMessages,
711
- system,
712
- tools2,
713
- model
714
- ).then((text) => {
715
- if (text) {
716
- summaries.push({ name, text });
717
- }
718
- })
704
+ if (candidates.length === 0) {
705
+ const available = headings.filter((h) => h.startLine >= searchStart && h.startLine < searchEnd).map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
706
+ const searchedPath = segments.slice(0, si + 1).join(" > ");
707
+ throw new Error(
708
+ `Heading not found: "${searchedPath}"
709
+
710
+ Available headings:
711
+ ${available || "(none)"}`
719
712
  );
720
713
  }
714
+ resolved = candidates[0];
715
+ searchStart = resolved.contentStart;
716
+ searchEnd = resolved.contentEnd;
721
717
  }
722
- await Promise.all(tasks);
723
- const checkpointMessages = summaries.map((s) => ({
724
- role: "user",
725
- hidden: true,
726
- content: [
727
- {
728
- type: "summary",
729
- name: s.name,
730
- text: s.text,
731
- startedAt: Date.now()
732
- }
733
- ]
734
- }));
735
- log3.info("Compaction complete", { summaries: summaries.length });
736
- return checkpointMessages;
718
+ return {
719
+ startLine: resolved.startLine,
720
+ contentStart: resolved.contentStart,
721
+ contentEnd: resolved.contentEnd
722
+ };
737
723
  }
738
- function findSafeInsertionPoint(messages) {
739
- let idx = messages.length;
740
- while (idx > 0) {
741
- const msg = messages[idx - 1];
742
- if (msg.role === "user" && msg.toolCallId) {
743
- idx--;
744
- } else {
745
- break;
746
- }
747
- }
748
- if (idx < messages.length && idx > 0) {
749
- const msg = messages[idx - 1];
750
- if (msg.role === "assistant" && Array.isArray(msg.content)) {
751
- const hasToolUse = msg.content.some(
752
- (b) => b.type === "tool"
753
- );
754
- if (hasToolUse) {
755
- idx--;
756
- }
757
- }
724
+ function validateSpecPath(filePath) {
725
+ if (!filePath.startsWith("src/")) {
726
+ throw new Error(`Spec tool paths must start with src/. Got: "${filePath}"`);
758
727
  }
759
- return idx;
728
+ return filePath;
760
729
  }
761
- function getConversationMessagesForSummary(messages, endIndex) {
762
- let startIdx = 0;
763
- for (let i = endIndex - 1; i >= 0; i--) {
764
- const msg = messages[i];
765
- if (!Array.isArray(msg.content)) {
766
- continue;
767
- }
768
- for (const block of msg.content) {
769
- if (block.type === "summary" && block.name === "conversation") {
770
- startIdx = i + 1;
771
- break;
772
- }
773
- }
774
- if (startIdx > 0) {
775
- break;
776
- }
730
+ function getHeadingTree(content) {
731
+ const headings = parseHeadings(content);
732
+ if (headings.length === 0) {
733
+ return "(no headings)";
777
734
  }
778
- return messages.slice(startIdx, endIndex);
735
+ return headings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
779
736
  }
780
- function getSubAgentMessagesForSummary(messages, subAgentName, endIndex) {
781
- let checkpointIdx = -1;
782
- for (let i = endIndex - 1; i >= 0; i--) {
783
- const msg = messages[i];
784
- if (!Array.isArray(msg.content)) {
785
- continue;
737
+
738
+ // src/tools/spec/readSpec.ts
739
+ var DEFAULT_MAX_LINES = 500;
740
+ var readSpecTool = {
741
+ clearable: true,
742
+ definition: {
743
+ name: "readSpec",
744
+ description: "Read a spec file from src/ with line numbers. Always read a spec file before editing it. Paths are relative to the project root and must start with src/ (e.g., src/app.md, src/interfaces/web.md).",
745
+ inputSchema: {
746
+ type: "object",
747
+ properties: {
748
+ path: {
749
+ type: "string",
750
+ description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
751
+ },
752
+ offset: {
753
+ type: "number",
754
+ description: "Line number to start reading from (1-indexed). Use a negative number to read from the end. Defaults to 1."
755
+ },
756
+ maxLines: {
757
+ type: "number",
758
+ description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
759
+ }
760
+ },
761
+ required: ["path"]
786
762
  }
787
- for (const block of msg.content) {
788
- if (block.type === "summary" && block.name === subAgentName) {
789
- checkpointIdx = i;
790
- break;
791
- }
763
+ },
764
+ async execute(input) {
765
+ try {
766
+ validateSpecPath(input.path);
767
+ } catch (err) {
768
+ return `Error: ${err.message}`;
792
769
  }
793
- if (checkpointIdx !== -1) {
794
- break;
770
+ try {
771
+ const content = await fs5.readFile(input.path, "utf-8");
772
+ const allLines = content.split("\n");
773
+ const totalLines = allLines.length;
774
+ const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES;
775
+ let startIdx;
776
+ if (input.offset && input.offset < 0) {
777
+ startIdx = Math.max(0, totalLines + input.offset);
778
+ } else {
779
+ startIdx = Math.max(0, (input.offset || 1) - 1);
780
+ }
781
+ const sliced = allLines.slice(startIdx, startIdx + maxLines);
782
+ const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
783
+ let result = numbered;
784
+ const endLine = startIdx + sliced.length;
785
+ const displayStart = startIdx + 1;
786
+ if (endLine < totalLines) {
787
+ result += `
788
+
789
+ (showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
790
+ }
791
+ return result;
792
+ } catch (err) {
793
+ return `Error reading file: ${err.message}`;
795
794
  }
796
795
  }
797
- const startIdx = checkpointIdx !== -1 ? checkpointIdx + 1 : 0;
798
- const collected = [];
799
- for (let i = startIdx; i < endIndex; i++) {
800
- const msg = messages[i];
801
- if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
802
- continue;
803
- }
804
- for (const block of msg.content) {
805
- if (block.type === "tool" && block.name === subAgentName && block.subAgentMessages?.length) {
806
- collected.push(...block.subAgentMessages);
807
- }
808
- }
809
- }
810
- return collected;
811
- }
812
- function serializeForSummary(messages) {
813
- return messages.map((msg) => {
814
- if (typeof msg.content === "string") {
815
- return `[${msg.role}]: ${msg.content}`;
816
- }
817
- if (!Array.isArray(msg.content)) {
818
- return `[${msg.role}]: (empty)`;
819
- }
820
- const blocks = msg.content;
821
- const parts = [];
822
- for (const block of blocks) {
823
- if (block.type === "text") {
824
- parts.push(block.text);
825
- } else if (block.type === "tool") {
826
- parts.push(
827
- `[tool: ${block.name}(${JSON.stringify(block.input).slice(0, 200)})] \u2192 ${(block.result ?? "").slice(0, 500)}`
828
- );
829
- }
830
- }
831
- return `[${msg.role}]: ${parts.join("\n")}`;
832
- }).join("\n\n");
833
- }
834
- var CHUNK_CHAR_LIMIT = 24e5;
835
- async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools, model) {
836
- const serialized = serializeForSummary(messagesToSummarize);
837
- if (!serialized.trim()) {
838
- return null;
839
- }
840
- if (serialized.length > CHUNK_CHAR_LIMIT && messagesToSummarize.length > 1) {
841
- const mid = Math.floor(messagesToSummarize.length / 2);
842
- log3.info("Chunking summary", {
843
- name,
844
- messageCount: messagesToSummarize.length,
845
- serializedLength: serialized.length
846
- });
847
- const [first, second] = await Promise.all([
848
- generateSummary(
849
- apiConfig,
850
- `${name} [pt1]`,
851
- compactionPrompt,
852
- messagesToSummarize.slice(0, mid),
853
- mainSystem,
854
- mainTools,
855
- model
856
- ),
857
- generateSummary(
858
- apiConfig,
859
- `${name} [pt2]`,
860
- compactionPrompt,
861
- messagesToSummarize.slice(mid),
862
- mainSystem,
863
- mainTools,
864
- model
865
- )
866
- ]);
867
- const parts = [first, second].filter((p) => !!p);
868
- return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
869
- }
870
- log3.info("Generating summary", {
871
- name,
872
- messageCount: messagesToSummarize.length,
873
- cacheReuse: !!mainSystem
874
- });
875
- let summaryText = "";
876
- const useMainCache = !!mainSystem;
877
- const system = useMainCache ? mainSystem : compactionPrompt;
878
- const tools2 = [];
879
- const userContent = useMainCache ? `${compactionPrompt}
880
-
881
- ---
882
-
883
- Conversation to summarize:
884
-
885
- ${serialized}` : serialized;
886
- const iterStart = Date.now();
887
- for await (const event of streamChat({
888
- ...apiConfig,
889
- model,
890
- subAgentId: "conversationSummarizer",
891
- system,
892
- messages: [{ role: "user", content: userContent }],
893
- tools: tools2
894
- })) {
895
- if (event.type === "text") {
896
- summaryText += event.text;
897
- } else if (event.type === "done") {
898
- recordUsage({
899
- ts: Date.now(),
900
- agentName: "conversationSummarizer",
901
- modelId: event.modelId,
902
- inputTokens: event.usage.inputTokens,
903
- outputTokens: event.usage.outputTokens,
904
- cacheCreationTokens: event.usage.cacheCreationTokens,
905
- cacheReadTokens: event.usage.cacheReadTokens,
906
- cost: nanoToDollars(event.cost),
907
- billingEvents: event.billingEvents,
908
- durationMs: Date.now() - iterStart,
909
- toolNames: []
910
- });
911
- } else if (event.type === "error") {
912
- log3.error("Summary generation failed", { name, error: event.error });
913
- return null;
914
- }
915
- }
916
- if (!summaryText.trim()) {
917
- log3.warn("Empty summary generated", { name });
918
- return null;
919
- }
920
- log3.info("Summary generated", { name, summaryLength: summaryText.length });
921
- return summaryText.trim();
922
- }
796
+ };
923
797
 
924
- // src/tools/spec/readSpec.ts
798
+ // src/tools/spec/writeSpec.ts
925
799
  import fs6 from "fs/promises";
800
+ import path4 from "path";
926
801
 
927
- // src/tools/spec/_helpers.ts
928
- var HEADING_RE = /^(#{1,6})\s+(.+)$/;
929
- function parseHeadings(content) {
930
- const lines = content.split("\n");
931
- const headings = [];
932
- for (let i = 0; i < lines.length; i++) {
933
- const match = lines[i].match(HEADING_RE);
934
- if (match) {
935
- headings.push({
936
- level: match[1].length,
937
- text: match[2].trim(),
938
- startLine: i,
939
- contentStart: i + 1,
940
- contentEnd: lines.length
941
- // placeholder — resolved below
942
- });
943
- }
802
+ // src/tools/_helpers/diff.ts
803
+ var CONTEXT_LINES = 3;
804
+ function unifiedDiff(filePath, oldText, newText) {
805
+ const oldLines = oldText ? oldText.split("\n") : [];
806
+ const newLines = newText ? newText.split("\n") : [];
807
+ let firstDiff = 0;
808
+ while (firstDiff < oldLines.length && firstDiff < newLines.length && oldLines[firstDiff] === newLines[firstDiff]) {
809
+ firstDiff++;
944
810
  }
945
- for (let i = 0; i < headings.length; i++) {
946
- const current = headings[i];
947
- let end = lines.length;
948
- for (let j = i + 1; j < headings.length; j++) {
949
- if (headings[j].level <= current.level) {
950
- end = headings[j].startLine;
951
- break;
952
- }
953
- }
954
- current.contentEnd = end;
811
+ let oldEnd = oldLines.length - 1;
812
+ let newEnd = newLines.length - 1;
813
+ while (oldEnd > firstDiff && newEnd > firstDiff && oldLines[oldEnd] === newLines[newEnd]) {
814
+ oldEnd--;
815
+ newEnd--;
955
816
  }
956
- return headings;
957
- }
958
- function resolveHeadingPath(content, headingPath) {
959
- const lines = content.split("\n");
960
- const headings = parseHeadings(content);
961
- if (headingPath === "") {
962
- const firstHeadingLine = headings.length > 0 ? headings[0].startLine : lines.length;
963
- return {
964
- startLine: 0,
965
- contentStart: 0,
966
- contentEnd: firstHeadingLine
967
- };
817
+ const ctxStart = Math.max(0, firstDiff - CONTEXT_LINES);
818
+ const ctxOldEnd = Math.min(oldLines.length - 1, oldEnd + CONTEXT_LINES);
819
+ const ctxNewEnd = Math.min(newLines.length - 1, newEnd + CONTEXT_LINES);
820
+ const lines = [];
821
+ lines.push(`--- ${filePath}`);
822
+ lines.push(`+++ ${filePath}`);
823
+ lines.push(
824
+ `@@ -${ctxStart + 1},${ctxOldEnd - ctxStart + 1} +${ctxStart + 1},${ctxNewEnd - ctxStart + 1} @@`
825
+ );
826
+ for (let i = ctxStart; i < firstDiff; i++) {
827
+ lines.push(` ${oldLines[i]}`);
968
828
  }
969
- const segments = headingPath.split(">").map((s) => s.trim());
970
- let searchStart = 0;
971
- let searchEnd = lines.length;
972
- let resolved = null;
973
- for (let si = 0; si < segments.length; si++) {
974
- const segment = segments[si].toLowerCase();
975
- const candidates = headings.filter(
976
- (h) => h.startLine >= searchStart && h.startLine < searchEnd && h.text.toLowerCase() === segment
977
- );
978
- if (candidates.length === 0) {
979
- const available = headings.filter((h) => h.startLine >= searchStart && h.startLine < searchEnd).map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
980
- const searchedPath = segments.slice(0, si + 1).join(" > ");
981
- throw new Error(
982
- `Heading not found: "${searchedPath}"
983
-
984
- Available headings:
985
- ${available || "(none)"}`
986
- );
987
- }
988
- resolved = candidates[0];
989
- searchStart = resolved.contentStart;
990
- searchEnd = resolved.contentEnd;
829
+ for (let i = firstDiff; i <= oldEnd; i++) {
830
+ lines.push(`-${oldLines[i]}`);
991
831
  }
992
- return {
993
- startLine: resolved.startLine,
994
- contentStart: resolved.contentStart,
995
- contentEnd: resolved.contentEnd
996
- };
997
- }
998
- function validateSpecPath(filePath) {
999
- if (!filePath.startsWith("src/")) {
1000
- throw new Error(`Spec tool paths must start with src/. Got: "${filePath}"`);
832
+ for (let i = firstDiff; i <= newEnd; i++) {
833
+ lines.push(`+${newLines[i]}`);
1001
834
  }
1002
- return filePath;
1003
- }
1004
- function getHeadingTree(content) {
1005
- const headings = parseHeadings(content);
1006
- if (headings.length === 0) {
1007
- return "(no headings)";
835
+ for (let i = oldEnd + 1; i <= ctxOldEnd; i++) {
836
+ lines.push(` ${oldLines[i]}`);
1008
837
  }
1009
- return headings.map((h) => `${"#".repeat(h.level)} ${h.text}`).join("\n");
838
+ return lines.join("\n");
1010
839
  }
1011
840
 
1012
- // src/tools/spec/readSpec.ts
1013
- var DEFAULT_MAX_LINES = 500;
1014
- var readSpecTool = {
841
+ // src/tools/_helpers/fileLock.ts
842
+ var locks = /* @__PURE__ */ new Map();
843
+ function acquireFileLock(filePath) {
844
+ let release;
845
+ const next = new Promise((res) => {
846
+ release = res;
847
+ });
848
+ const wait = locks.get(filePath) ?? Promise.resolve();
849
+ locks.set(filePath, next);
850
+ return wait.then(() => release);
851
+ }
852
+
853
+ // src/tools/spec/writeSpec.ts
854
+ var writeSpecTool = {
1015
855
  clearable: true,
1016
856
  definition: {
1017
- name: "readSpec",
1018
- description: "Read a spec file from src/ with line numbers. Always read a spec file before editing it. Paths are relative to the project root and must start with src/ (e.g., src/app.md, src/interfaces/web.md).",
857
+ name: "writeSpec",
858
+ description: "Create a new spec file or completely overwrite an existing one in src/. Parent directories are created automatically. Use this for new spec files or full rewrites. For targeted changes to existing specs, use editSpec instead.",
1019
859
  inputSchema: {
1020
860
  type: "object",
1021
861
  properties: {
@@ -1023,131 +863,17 @@ var readSpecTool = {
1023
863
  type: "string",
1024
864
  description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
1025
865
  },
1026
- offset: {
1027
- type: "number",
1028
- description: "Line number to start reading from (1-indexed). Use a negative number to read from the end. Defaults to 1."
1029
- },
1030
- maxLines: {
1031
- type: "number",
1032
- description: "Maximum number of lines to return. Defaults to 500. Set to 0 for no limit."
866
+ content: {
867
+ type: "string",
868
+ description: "The full MSFM markdown content to write."
1033
869
  }
1034
870
  },
1035
- required: ["path"]
1036
- }
1037
- },
1038
- async execute(input) {
1039
- try {
1040
- validateSpecPath(input.path);
1041
- } catch (err) {
1042
- return `Error: ${err.message}`;
1043
- }
1044
- try {
1045
- const content = await fs6.readFile(input.path, "utf-8");
1046
- const allLines = content.split("\n");
1047
- const totalLines = allLines.length;
1048
- const maxLines = input.maxLines === 0 ? Infinity : input.maxLines || DEFAULT_MAX_LINES;
1049
- let startIdx;
1050
- if (input.offset && input.offset < 0) {
1051
- startIdx = Math.max(0, totalLines + input.offset);
1052
- } else {
1053
- startIdx = Math.max(0, (input.offset || 1) - 1);
1054
- }
1055
- const sliced = allLines.slice(startIdx, startIdx + maxLines);
1056
- const numbered = sliced.map((line, i) => `${String(startIdx + i + 1).padStart(4)} ${line}`).join("\n");
1057
- let result = numbered;
1058
- const endLine = startIdx + sliced.length;
1059
- const displayStart = startIdx + 1;
1060
- if (endLine < totalLines) {
1061
- result += `
1062
-
1063
- (showing lines ${displayStart}\u2013${endLine} of ${totalLines} \u2014 use offset and maxLines to read more)`;
1064
- }
1065
- return result;
1066
- } catch (err) {
1067
- return `Error reading file: ${err.message}`;
1068
- }
1069
- }
1070
- };
1071
-
1072
- // src/tools/spec/writeSpec.ts
1073
- import fs7 from "fs/promises";
1074
- import path4 from "path";
1075
-
1076
- // src/tools/_helpers/diff.ts
1077
- var CONTEXT_LINES = 3;
1078
- function unifiedDiff(filePath, oldText, newText) {
1079
- const oldLines = oldText ? oldText.split("\n") : [];
1080
- const newLines = newText ? newText.split("\n") : [];
1081
- let firstDiff = 0;
1082
- while (firstDiff < oldLines.length && firstDiff < newLines.length && oldLines[firstDiff] === newLines[firstDiff]) {
1083
- firstDiff++;
1084
- }
1085
- let oldEnd = oldLines.length - 1;
1086
- let newEnd = newLines.length - 1;
1087
- while (oldEnd > firstDiff && newEnd > firstDiff && oldLines[oldEnd] === newLines[newEnd]) {
1088
- oldEnd--;
1089
- newEnd--;
1090
- }
1091
- const ctxStart = Math.max(0, firstDiff - CONTEXT_LINES);
1092
- const ctxOldEnd = Math.min(oldLines.length - 1, oldEnd + CONTEXT_LINES);
1093
- const ctxNewEnd = Math.min(newLines.length - 1, newEnd + CONTEXT_LINES);
1094
- const lines = [];
1095
- lines.push(`--- ${filePath}`);
1096
- lines.push(`+++ ${filePath}`);
1097
- lines.push(
1098
- `@@ -${ctxStart + 1},${ctxOldEnd - ctxStart + 1} +${ctxStart + 1},${ctxNewEnd - ctxStart + 1} @@`
1099
- );
1100
- for (let i = ctxStart; i < firstDiff; i++) {
1101
- lines.push(` ${oldLines[i]}`);
1102
- }
1103
- for (let i = firstDiff; i <= oldEnd; i++) {
1104
- lines.push(`-${oldLines[i]}`);
1105
- }
1106
- for (let i = firstDiff; i <= newEnd; i++) {
1107
- lines.push(`+${newLines[i]}`);
1108
- }
1109
- for (let i = oldEnd + 1; i <= ctxOldEnd; i++) {
1110
- lines.push(` ${oldLines[i]}`);
1111
- }
1112
- return lines.join("\n");
1113
- }
1114
-
1115
- // src/tools/_helpers/fileLock.ts
1116
- var locks = /* @__PURE__ */ new Map();
1117
- function acquireFileLock(filePath) {
1118
- let release;
1119
- const next = new Promise((res) => {
1120
- release = res;
1121
- });
1122
- const wait = locks.get(filePath) ?? Promise.resolve();
1123
- locks.set(filePath, next);
1124
- return wait.then(() => release);
1125
- }
1126
-
1127
- // src/tools/spec/writeSpec.ts
1128
- var writeSpecTool = {
1129
- clearable: true,
1130
- definition: {
1131
- name: "writeSpec",
1132
- description: "Create a new spec file or completely overwrite an existing one in src/. Parent directories are created automatically. Use this for new spec files or full rewrites. For targeted changes to existing specs, use editSpec instead.",
1133
- inputSchema: {
1134
- type: "object",
1135
- properties: {
1136
- path: {
1137
- type: "string",
1138
- description: "File path relative to project root, must start with src/ (e.g., src/app.md)."
1139
- },
1140
- content: {
1141
- type: "string",
1142
- description: "The full MSFM markdown content to write."
1143
- }
1144
- },
1145
- required: ["path", "content"]
871
+ required: ["path", "content"]
1146
872
  }
1147
873
  },
1148
874
  streaming: {
1149
875
  transform: async (partial) => {
1150
- const oldContent = await fs7.readFile(partial.path, "utf-8").catch(() => "");
876
+ const oldContent = await fs6.readFile(partial.path, "utf-8").catch(() => "");
1151
877
  const lineCount = partial.content.split("\n").length;
1152
878
  return `Writing ${partial.path} (${lineCount} lines)
1153
879
  ${unifiedDiff(partial.path, oldContent, partial.content)}`;
@@ -1161,13 +887,13 @@ ${unifiedDiff(partial.path, oldContent, partial.content)}`;
1161
887
  }
1162
888
  const release = await acquireFileLock(input.path);
1163
889
  try {
1164
- await fs7.mkdir(path4.dirname(input.path), { recursive: true });
890
+ await fs6.mkdir(path4.dirname(input.path), { recursive: true });
1165
891
  let oldContent = null;
1166
892
  try {
1167
- oldContent = await fs7.readFile(input.path, "utf-8");
893
+ oldContent = await fs6.readFile(input.path, "utf-8");
1168
894
  } catch {
1169
895
  }
1170
- await fs7.writeFile(input.path, input.content, "utf-8");
896
+ await fs6.writeFile(input.path, input.content, "utf-8");
1171
897
  const lineCount = input.content.split("\n").length;
1172
898
  const label = oldContent !== null ? "Wrote" : "Created";
1173
899
  return `${label} ${input.path} (${lineCount} lines)
@@ -1181,7 +907,7 @@ ${unifiedDiff(input.path, oldContent ?? "", input.content)}`;
1181
907
  };
1182
908
 
1183
909
  // src/tools/spec/editSpec.ts
1184
- import fs8 from "fs/promises";
910
+ import fs7 from "fs/promises";
1185
911
  var editSpecTool = {
1186
912
  clearable: true,
1187
913
  definition: {
@@ -1231,7 +957,7 @@ var editSpecTool = {
1231
957
  try {
1232
958
  let originalContent;
1233
959
  try {
1234
- originalContent = await fs8.readFile(input.path, "utf-8");
960
+ originalContent = await fs7.readFile(input.path, "utf-8");
1235
961
  } catch (err) {
1236
962
  return `Error reading file: ${err.message}`;
1237
963
  }
@@ -1287,7 +1013,7 @@ ${tree}`;
1287
1013
  content = lines.join("\n");
1288
1014
  }
1289
1015
  try {
1290
- await fs8.writeFile(input.path, content, "utf-8");
1016
+ await fs7.writeFile(input.path, content, "utf-8");
1291
1017
  } catch (err) {
1292
1018
  return `Error writing file: ${err.message}`;
1293
1019
  }
@@ -1299,7 +1025,7 @@ ${tree}`;
1299
1025
  };
1300
1026
 
1301
1027
  // src/tools/spec/listSpecFiles.ts
1302
- import fs9 from "fs/promises";
1028
+ import fs8 from "fs/promises";
1303
1029
  import path5 from "path";
1304
1030
  var listSpecFilesTool = {
1305
1031
  clearable: false,
@@ -1329,7 +1055,7 @@ var listSpecFilesTool = {
1329
1055
  };
1330
1056
  async function listRecursive(dir) {
1331
1057
  const results = [];
1332
- const entries = await fs9.readdir(dir, { withFileTypes: true });
1058
+ const entries = await fs8.readdir(dir, { withFileTypes: true });
1333
1059
  entries.sort((a, b) => {
1334
1060
  if (a.isDirectory() && !b.isDirectory()) {
1335
1061
  return -1;
@@ -1375,7 +1101,7 @@ var presentPublishPlanTool = {
1375
1101
  };
1376
1102
 
1377
1103
  // src/tools/spec/writePlan.ts
1378
- import fs10 from "fs/promises";
1104
+ import fs9 from "fs/promises";
1379
1105
  var PLAN_FILE = ".remy-plan.md";
1380
1106
  var writePlanTool = {
1381
1107
  clearable: false,
@@ -1400,13 +1126,13 @@ status: pending
1400
1126
  ---
1401
1127
 
1402
1128
  ${content}`;
1403
- await fs10.writeFile(PLAN_FILE, file, "utf-8");
1129
+ await fs9.writeFile(PLAN_FILE, file, "utf-8");
1404
1130
  return "Plan written to .remy-plan.md. Waiting for user approval.";
1405
1131
  }
1406
1132
  };
1407
1133
 
1408
1134
  // src/tools/spec/updatePlanStatus.ts
1409
- import fs11 from "fs/promises";
1135
+ import fs10 from "fs/promises";
1410
1136
  var PLAN_FILE2 = ".remy-plan.md";
1411
1137
  var updatePlanStatusTool = {
1412
1138
  clearable: false,
@@ -1429,15 +1155,15 @@ var updatePlanStatusTool = {
1429
1155
  const status = input.status;
1430
1156
  let content;
1431
1157
  try {
1432
- content = await fs11.readFile(PLAN_FILE2, "utf-8");
1158
+ content = await fs10.readFile(PLAN_FILE2, "utf-8");
1433
1159
  } catch {
1434
1160
  return "No plan file found.";
1435
1161
  }
1436
1162
  if (status === "rejected") {
1437
- await fs11.unlink(PLAN_FILE2);
1163
+ await fs10.unlink(PLAN_FILE2);
1438
1164
  return "Plan rejected and removed.";
1439
1165
  }
1440
- await fs11.writeFile(
1166
+ await fs10.writeFile(
1441
1167
  PLAN_FILE2,
1442
1168
  content.replace(/^status:\s*\w+/m, `status: ${status}`),
1443
1169
  "utf-8"
@@ -1743,6 +1469,24 @@ var askMindStudioSdkTool = {
1743
1469
  }
1744
1470
  };
1745
1471
 
1472
+ // src/usageLedger.ts
1473
+ import fs11 from "fs";
1474
+ var LEDGER_FILE = ".logs/usage.ndjson";
1475
+ var fd = null;
1476
+ function nanoToDollars(nano) {
1477
+ return typeof nano === "number" ? nano / 1e9 : void 0;
1478
+ }
1479
+ function recordUsage(entry) {
1480
+ try {
1481
+ if (fd === null) {
1482
+ fs11.mkdirSync(".logs", { recursive: true });
1483
+ fd = fs11.openSync(LEDGER_FILE, "a");
1484
+ }
1485
+ fs11.writeSync(fd, JSON.stringify(entry) + "\n");
1486
+ } catch {
1487
+ }
1488
+ }
1489
+
1746
1490
  // src/subagents/common/runMindstudioCli.ts
1747
1491
  function stripFlags(args) {
1748
1492
  const out = [];
@@ -2584,11 +2328,11 @@ var editsFinishedTool = {
2584
2328
  };
2585
2329
 
2586
2330
  // src/tools/_helpers/sidecar.ts
2587
- var log4 = createLogger("sidecar");
2331
+ var log3 = createLogger("sidecar");
2588
2332
  var baseUrl = null;
2589
2333
  function setSidecarBaseUrl(url) {
2590
2334
  baseUrl = url;
2591
- log4.info("Configured", { url });
2335
+ log3.info("Configured", { url });
2592
2336
  }
2593
2337
  async function sidecarRequest(endpoint, body = {}, options) {
2594
2338
  if (!baseUrl) {
@@ -2603,7 +2347,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
2603
2347
  signal: options?.timeout ? AbortSignal.timeout(options.timeout) : void 0
2604
2348
  });
2605
2349
  if (!res.ok) {
2606
- log4.error("Sidecar error", { endpoint, status: res.status });
2350
+ log3.error("Sidecar error", { endpoint, status: res.status });
2607
2351
  throw new Error(`Sidecar error: ${res.status}`);
2608
2352
  }
2609
2353
  const data = await res.json();
@@ -2616,7 +2360,7 @@ async function sidecarRequest(endpoint, body = {}, options) {
2616
2360
  if (err.message.startsWith("Sidecar error")) {
2617
2361
  throw err;
2618
2362
  }
2619
- log4.error("Sidecar connection error", { endpoint, error: err.message });
2363
+ log3.error("Sidecar connection error", { endpoint, error: err.message });
2620
2364
  throw new Error(`Sidecar connection error: ${err.message}`);
2621
2365
  }
2622
2366
  }
@@ -2907,7 +2651,7 @@ function acquireBrowserLock() {
2907
2651
  }
2908
2652
 
2909
2653
  // src/toolRegistry.ts
2910
- var log5 = createLogger("tool-registry");
2654
+ var log4 = createLogger("tool-registry");
2911
2655
  var USER_CANCELLED_RESULT = "[USER CANCELLED] The user manually cancelled this tool. Do not retry it automatically \u2014 wait for the user\u2019s next message for direction.";
2912
2656
  var ToolRegistry = class {
2913
2657
  entries = /* @__PURE__ */ new Map();
@@ -2934,7 +2678,7 @@ var ToolRegistry = class {
2934
2678
  if (!entry) {
2935
2679
  return false;
2936
2680
  }
2937
- log5.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
2681
+ log4.info("Tool stopped", { toolCallId: id, name: entry.name, mode });
2938
2682
  entry.abortController.abort(mode);
2939
2683
  if (mode === "graceful") {
2940
2684
  const partial = entry.getPartialResult?.() ?? "";
@@ -2967,7 +2711,7 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
2967
2711
  if (!entry) {
2968
2712
  return false;
2969
2713
  }
2970
- log5.info("Tool restarted", { toolCallId: id, name: entry.name });
2714
+ log4.info("Tool restarted", { toolCallId: id, name: entry.name });
2971
2715
  entry.abortController.abort("restart");
2972
2716
  const newInput = patchedInput ? { ...entry.input, ...patchedInput } : entry.input;
2973
2717
  this.onEvent?.({
@@ -3227,7 +2971,7 @@ ${content}` : attachmentHeader;
3227
2971
  }
3228
2972
 
3229
2973
  // src/subagents/runner.ts
3230
- var log6 = createLogger("sub-agent");
2974
+ var log5 = createLogger("sub-agent");
3231
2975
  async function runSubAgent(config) {
3232
2976
  const {
3233
2977
  system,
@@ -3254,7 +2998,7 @@ async function runSubAgent(config) {
3254
2998
  const signal = background ? bgAbort.signal : parentSignal;
3255
2999
  const agentName = subAgentId || "sub-agent";
3256
3000
  const runStart = Date.now();
3257
- log6.info("Sub-agent started", { requestId, parentToolId, agentName });
3001
+ log5.info("Sub-agent started", { requestId, parentToolId, agentName });
3258
3002
  const emit = (e) => {
3259
3003
  onEvent({ ...e, parentToolId });
3260
3004
  };
@@ -3471,7 +3215,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3471
3215
  ...hasArtifacts ? { artifacts } : {}
3472
3216
  };
3473
3217
  }
3474
- log6.info("Tools executing", {
3218
+ log5.info("Tools executing", {
3475
3219
  requestId,
3476
3220
  parentToolId,
3477
3221
  count: toolCalls.length,
@@ -3548,7 +3292,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3548
3292
  run2(tc.input);
3549
3293
  const r = await resultPromise;
3550
3294
  toolRegistry?.unregister(tc.id);
3551
- log6.info("Tool completed", {
3295
+ log5.info("Tool completed", {
3552
3296
  requestId,
3553
3297
  parentToolId,
3554
3298
  toolCallId: tc.id,
@@ -3599,7 +3343,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3599
3343
  const wrapRun = async () => {
3600
3344
  try {
3601
3345
  const result = await run();
3602
- log6.info("Sub-agent complete", {
3346
+ log5.info("Sub-agent complete", {
3603
3347
  requestId,
3604
3348
  parentToolId,
3605
3349
  agentName,
@@ -3608,7 +3352,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3608
3352
  });
3609
3353
  return result;
3610
3354
  } catch (err) {
3611
- log6.warn("Sub-agent error", {
3355
+ log5.warn("Sub-agent error", {
3612
3356
  requestId,
3613
3357
  parentToolId,
3614
3358
  agentName,
@@ -3620,7 +3364,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3620
3364
  if (!background) {
3621
3365
  return wrapRun();
3622
3366
  }
3623
- log6.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3367
+ log5.info("Sub-agent backgrounded", { requestId, parentToolId, agentName });
3624
3368
  toolRegistry?.register({
3625
3369
  id: parentToolId,
3626
3370
  name: agentName,
@@ -3888,6 +3632,7 @@ var ALLOWED_MODELS_BY_TYPE = {
3888
3632
  "claude-4-7-opus",
3889
3633
  "claude-4-6-opus",
3890
3634
  "claude-4-6-sonnet",
3635
+ "claude-fable-5",
3891
3636
  "gpt-5.5",
3892
3637
  "gemini-3-pro",
3893
3638
  "gemini-3.1-pro",
@@ -3903,7 +3648,7 @@ function resolveModel(surfaceId, models, fallback) {
3903
3648
  }
3904
3649
 
3905
3650
  // src/subagents/browserAutomation/index.ts
3906
- var log7 = createLogger("browser-automation");
3651
+ var log6 = createLogger("browser-automation");
3907
3652
  async function runBrowserAutomation(task, context, opts) {
3908
3653
  const release = await acquireBrowserLock();
3909
3654
  try {
@@ -4002,7 +3747,7 @@ async function runBrowserAutomation(task, context, opts) {
4002
3747
  }
4003
3748
  }
4004
3749
  } catch {
4005
- log7.debug("Failed to parse batch analysis result", {
3750
+ log6.debug("Failed to parse batch analysis result", {
4006
3751
  batchResult
4007
3752
  });
4008
3753
  }
@@ -5676,6 +5421,13 @@ var ALL_TOOLS = [
5676
5421
  var CLEARABLE_TOOLS = new Set(
5677
5422
  ALL_TOOLS.filter((t) => t.clearable).map((t) => t.definition.name)
5678
5423
  );
5424
+ var SUBAGENT_TOOL_NAMES = /* @__PURE__ */ new Set([
5425
+ "visualDesignExpert",
5426
+ "productVision",
5427
+ "codeSanityCheck",
5428
+ "runAutomatedBrowserTest",
5429
+ "askMindStudioSdk"
5430
+ ]);
5679
5431
  function getToolDefinitions(_onboardingState) {
5680
5432
  return ALL_TOOLS.map((t) => t.definition);
5681
5433
  }
@@ -5690,6 +5442,295 @@ function executeTool(name, input, context) {
5690
5442
  return tool.execute(input, context);
5691
5443
  }
5692
5444
 
5445
+ // src/compaction/index.ts
5446
+ var log7 = createLogger("compaction");
5447
+ var CONVERSATION_SUMMARY_PROMPT = readAsset("compaction", "conversation.md");
5448
+ var SUBAGENT_SUMMARY_PROMPT = readAsset("compaction", "subagent.md");
5449
+ var SUMMARIZABLE_SUBAGENTS = ["visualDesignExpert", "productVision"];
5450
+ async function compactConversation(messages, apiConfig, system, tools2, model) {
5451
+ const endIndex = findSafeInsertionPoint(messages);
5452
+ const summaries = [];
5453
+ const tasks = [];
5454
+ const conversationMessages = getConversationMessagesForSummary(
5455
+ messages,
5456
+ endIndex
5457
+ );
5458
+ if (conversationMessages.length > 0) {
5459
+ tasks.push(
5460
+ generateSummary(
5461
+ apiConfig,
5462
+ "conversation",
5463
+ CONVERSATION_SUMMARY_PROMPT,
5464
+ conversationMessages,
5465
+ system,
5466
+ tools2,
5467
+ model
5468
+ ).then((text) => {
5469
+ if (text) {
5470
+ summaries.push({ name: "conversation", text });
5471
+ }
5472
+ })
5473
+ );
5474
+ }
5475
+ for (const name of SUMMARIZABLE_SUBAGENTS) {
5476
+ const subagentMessages = getSubAgentMessagesForSummary(
5477
+ messages,
5478
+ name,
5479
+ endIndex
5480
+ );
5481
+ if (subagentMessages.length > 0) {
5482
+ tasks.push(
5483
+ generateSummary(
5484
+ apiConfig,
5485
+ name,
5486
+ SUBAGENT_SUMMARY_PROMPT,
5487
+ subagentMessages,
5488
+ system,
5489
+ tools2,
5490
+ model
5491
+ ).then((text) => {
5492
+ if (text) {
5493
+ summaries.push({ name, text });
5494
+ }
5495
+ })
5496
+ );
5497
+ }
5498
+ }
5499
+ await Promise.all(tasks);
5500
+ const checkpointMessages = summaries.map((s) => ({
5501
+ role: "user",
5502
+ hidden: true,
5503
+ content: [
5504
+ {
5505
+ type: "summary",
5506
+ name: s.name,
5507
+ text: s.text,
5508
+ startedAt: Date.now()
5509
+ }
5510
+ ]
5511
+ }));
5512
+ log7.info("Compaction complete", { summaries: summaries.length });
5513
+ return checkpointMessages;
5514
+ }
5515
+ function findSafeInsertionPoint(messages) {
5516
+ let idx = messages.length;
5517
+ while (idx > 0) {
5518
+ const msg = messages[idx - 1];
5519
+ if (msg.role === "user" && msg.toolCallId) {
5520
+ idx--;
5521
+ } else {
5522
+ break;
5523
+ }
5524
+ }
5525
+ if (idx < messages.length && idx > 0) {
5526
+ const msg = messages[idx - 1];
5527
+ if (msg.role === "assistant" && Array.isArray(msg.content)) {
5528
+ const hasToolUse = msg.content.some(
5529
+ (b) => b.type === "tool"
5530
+ );
5531
+ if (hasToolUse) {
5532
+ idx--;
5533
+ }
5534
+ }
5535
+ }
5536
+ return idx;
5537
+ }
5538
+ function getConversationMessagesForSummary(messages, endIndex) {
5539
+ let startIdx = 0;
5540
+ for (let i = endIndex - 1; i >= 0; i--) {
5541
+ const msg = messages[i];
5542
+ if (!Array.isArray(msg.content)) {
5543
+ continue;
5544
+ }
5545
+ for (const block of msg.content) {
5546
+ if (block.type === "summary" && block.name === "conversation") {
5547
+ startIdx = i + 1;
5548
+ break;
5549
+ }
5550
+ }
5551
+ if (startIdx > 0) {
5552
+ break;
5553
+ }
5554
+ }
5555
+ return messages.slice(startIdx, endIndex);
5556
+ }
5557
+ function getSubAgentMessagesForSummary(messages, subAgentName, endIndex) {
5558
+ let checkpointIdx = -1;
5559
+ for (let i = endIndex - 1; i >= 0; i--) {
5560
+ const msg = messages[i];
5561
+ if (!Array.isArray(msg.content)) {
5562
+ continue;
5563
+ }
5564
+ for (const block of msg.content) {
5565
+ if (block.type === "summary" && block.name === subAgentName) {
5566
+ checkpointIdx = i;
5567
+ break;
5568
+ }
5569
+ }
5570
+ if (checkpointIdx !== -1) {
5571
+ break;
5572
+ }
5573
+ }
5574
+ const startIdx = checkpointIdx !== -1 ? checkpointIdx + 1 : 0;
5575
+ const collected = [];
5576
+ for (let i = startIdx; i < endIndex; i++) {
5577
+ const msg = messages[i];
5578
+ if (msg.role !== "assistant" || !Array.isArray(msg.content)) {
5579
+ continue;
5580
+ }
5581
+ for (const block of msg.content) {
5582
+ if (block.type === "tool" && block.name === subAgentName && block.subAgentMessages?.length) {
5583
+ collected.push(...block.subAgentMessages);
5584
+ }
5585
+ }
5586
+ }
5587
+ return collected;
5588
+ }
5589
+ function serializeForSummary(messages) {
5590
+ const toolNameById = /* @__PURE__ */ new Map();
5591
+ for (const msg of messages) {
5592
+ if (!Array.isArray(msg.content)) {
5593
+ continue;
5594
+ }
5595
+ for (const block of msg.content) {
5596
+ if (block.type === "tool") {
5597
+ toolNameById.set(block.id, block.name);
5598
+ }
5599
+ }
5600
+ }
5601
+ const lines = [];
5602
+ for (const msg of messages) {
5603
+ if (msg.role === "user" && msg.toolCallId) {
5604
+ const toolName = toolNameById.get(msg.toolCallId);
5605
+ if (toolName && SUBAGENT_TOOL_NAMES.has(toolName)) {
5606
+ const content = typeof msg.content === "string" ? msg.content : Array.isArray(msg.content) ? msg.content.filter(
5607
+ (b) => b.type === "text"
5608
+ ).map((b) => b.text).join("\n") : "";
5609
+ if (content.trim()) {
5610
+ lines.push(`[${toolName} result]: ${content}`);
5611
+ }
5612
+ }
5613
+ continue;
5614
+ }
5615
+ if (typeof msg.content === "string") {
5616
+ if (msg.content.trim()) {
5617
+ lines.push(`[${msg.role}]: ${msg.content}`);
5618
+ }
5619
+ continue;
5620
+ }
5621
+ if (!Array.isArray(msg.content)) {
5622
+ continue;
5623
+ }
5624
+ const blocks = msg.content;
5625
+ const parts = [];
5626
+ let toolCount = 0;
5627
+ for (const block of blocks) {
5628
+ if (block.type === "text") {
5629
+ parts.push(block.text);
5630
+ } else if (block.type === "tool") {
5631
+ toolCount++;
5632
+ }
5633
+ }
5634
+ if (toolCount > 0) {
5635
+ parts.push(`[used ${toolCount} tool${toolCount === 1 ? "" : "s"}]`);
5636
+ }
5637
+ const body = parts.join("\n").trim();
5638
+ if (body) {
5639
+ lines.push(`[${msg.role}]: ${body}`);
5640
+ }
5641
+ }
5642
+ return lines.join("\n\n");
5643
+ }
5644
+ var CHUNK_CHAR_LIMIT = 24e5;
5645
+ async function generateSummary(apiConfig, name, compactionPrompt, messagesToSummarize, mainSystem, mainTools, model) {
5646
+ const serialized = serializeForSummary(messagesToSummarize);
5647
+ if (!serialized.trim()) {
5648
+ return null;
5649
+ }
5650
+ if (serialized.length > CHUNK_CHAR_LIMIT && messagesToSummarize.length > 1) {
5651
+ const mid = Math.floor(messagesToSummarize.length / 2);
5652
+ log7.info("Chunking summary", {
5653
+ name,
5654
+ messageCount: messagesToSummarize.length,
5655
+ serializedLength: serialized.length
5656
+ });
5657
+ const [first, second] = await Promise.all([
5658
+ generateSummary(
5659
+ apiConfig,
5660
+ `${name} [pt1]`,
5661
+ compactionPrompt,
5662
+ messagesToSummarize.slice(0, mid),
5663
+ mainSystem,
5664
+ mainTools,
5665
+ model
5666
+ ),
5667
+ generateSummary(
5668
+ apiConfig,
5669
+ `${name} [pt2]`,
5670
+ compactionPrompt,
5671
+ messagesToSummarize.slice(mid),
5672
+ mainSystem,
5673
+ mainTools,
5674
+ model
5675
+ )
5676
+ ]);
5677
+ const parts = [first, second].filter((p) => !!p);
5678
+ return parts.length > 0 ? parts.join("\n\n---\n\n") : null;
5679
+ }
5680
+ log7.info("Generating summary", {
5681
+ name,
5682
+ messageCount: messagesToSummarize.length,
5683
+ cacheReuse: !!mainSystem
5684
+ });
5685
+ let summaryText = "";
5686
+ const useMainCache = !!mainSystem;
5687
+ const system = useMainCache ? mainSystem : compactionPrompt;
5688
+ const tools2 = [];
5689
+ const userContent = useMainCache ? `${compactionPrompt}
5690
+
5691
+ ---
5692
+
5693
+ Conversation to summarize:
5694
+
5695
+ ${serialized}` : serialized;
5696
+ const iterStart = Date.now();
5697
+ for await (const event of streamChat({
5698
+ ...apiConfig,
5699
+ model,
5700
+ subAgentId: "conversationSummarizer",
5701
+ system,
5702
+ messages: [{ role: "user", content: userContent }],
5703
+ tools: tools2
5704
+ })) {
5705
+ if (event.type === "text") {
5706
+ summaryText += event.text;
5707
+ } else if (event.type === "done") {
5708
+ recordUsage({
5709
+ ts: Date.now(),
5710
+ agentName: "conversationSummarizer",
5711
+ modelId: event.modelId,
5712
+ inputTokens: event.usage.inputTokens,
5713
+ outputTokens: event.usage.outputTokens,
5714
+ cacheCreationTokens: event.usage.cacheCreationTokens,
5715
+ cacheReadTokens: event.usage.cacheReadTokens,
5716
+ cost: nanoToDollars(event.cost),
5717
+ billingEvents: event.billingEvents,
5718
+ durationMs: Date.now() - iterStart,
5719
+ toolNames: []
5720
+ });
5721
+ } else if (event.type === "error") {
5722
+ log7.error("Summary generation failed", { name, error: event.error });
5723
+ return null;
5724
+ }
5725
+ }
5726
+ if (!summaryText.trim()) {
5727
+ log7.warn("Empty summary generated", { name });
5728
+ return null;
5729
+ }
5730
+ log7.info("Summary generated", { name, summaryLength: summaryText.length });
5731
+ return summaryText.trim();
5732
+ }
5733
+
5693
5734
  // src/compaction/trigger.ts
5694
5735
  var log8 = createLogger("compaction:trigger");
5695
5736
  var pendingSummaries = [];