opencode-lore 0.4.1 → 0.4.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-lore",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Three-tier memory architecture for OpenCode — distillation, not summarization",
@@ -21,6 +21,17 @@ export const LORE_SECTION_START =
21
21
  "<!-- This section is maintained by the coding agent via lore (https://github.com/BYK/opencode-lore) -->";
22
22
  export const LORE_SECTION_END = "<!-- End lore-managed section -->";
23
23
 
24
+ /**
25
+ * All known start-marker variants, ordered newest-first.
26
+ * When we renamed the marker in the past, old files kept the old text.
27
+ * splitFile() matches any of these so it can strip all lore sections
28
+ * regardless of which marker version was used to write them.
29
+ */
30
+ const ALL_START_MARKERS = [
31
+ LORE_SECTION_START,
32
+ "<!-- This section is auto-maintained by lore (https://github.com/BYK/opencode-lore) -->",
33
+ ] as const;
34
+
24
35
  /** Regex matching a valid UUID (v4 or v7) — 8-4-4-4-12 hex groups. */
25
36
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
26
37
 
@@ -45,26 +56,70 @@ export type ParsedFileEntry = {
45
56
 
46
57
  /**
47
58
  * Split file content into three parts: before, lore section body, after.
48
- * Returns null for section body when markers are absent.
59
+ * Returns null for section body when no lore markers are found.
60
+ *
61
+ * Handles multiple lore sections (from duplication bugs) and all known
62
+ * start-marker variants (old + new text) by:
63
+ * - Collecting every lore section span in the file
64
+ * - Returning `before` = content before the first section
65
+ * - Returning `after` = content after the last section (all intermediate
66
+ * sections are discarded)
67
+ * - Returning `section` = body of the first section found (for import
68
+ * and shouldImport to read the canonical content)
69
+ *
70
+ * This is self-healing: a file with N duplicate sections will be collapsed
71
+ * to exactly one on the next exportToFile() call.
49
72
  */
50
73
  function splitFile(fileContent: string): {
51
74
  before: string;
52
75
  section: string | null;
53
76
  after: string;
54
77
  } {
55
- const startIdx = fileContent.indexOf(LORE_SECTION_START);
56
- const endIdx = fileContent.indexOf(LORE_SECTION_END);
78
+ // Collect every lore section span in the file, matching all known
79
+ // start-marker variants (current + historical renamed markers).
80
+ // Each span records: where the section body begins/ends and where the
81
+ // full span (including end-marker) ends.
82
+ type Span = { markerStart: number; bodyStart: number; bodyEnd: number; spanEnd: number };
83
+ const spans: Span[] = [];
84
+
85
+ let searchFrom = 0;
86
+ while (searchFrom < fileContent.length) {
87
+ // Find the earliest occurrence of any known start marker
88
+ let markerStart = -1;
89
+ let markerLen = 0;
90
+ for (const marker of ALL_START_MARKERS) {
91
+ const idx = fileContent.indexOf(marker, searchFrom);
92
+ if (idx !== -1 && (markerStart === -1 || idx < markerStart)) {
93
+ markerStart = idx;
94
+ markerLen = marker.length;
95
+ }
96
+ }
97
+ if (markerStart === -1) break; // no more start markers
98
+
99
+ const bodyStart = markerStart + markerLen;
100
+ const endIdx = fileContent.indexOf(LORE_SECTION_END, bodyStart);
101
+ if (endIdx === -1) {
102
+ // Unclosed section — consume to EOF
103
+ spans.push({ markerStart, bodyStart, bodyEnd: fileContent.length, spanEnd: fileContent.length });
104
+ break;
105
+ }
57
106
 
58
- if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
107
+ spans.push({ markerStart, bodyStart, bodyEnd: endIdx, spanEnd: endIdx + LORE_SECTION_END.length });
108
+ searchFrom = endIdx + LORE_SECTION_END.length;
109
+ }
110
+
111
+ if (spans.length === 0) {
59
112
  return { before: fileContent, section: null, after: "" };
60
113
  }
61
114
 
62
- const before = fileContent.slice(0, startIdx);
63
- const section = fileContent.slice(
64
- startIdx + LORE_SECTION_START.length,
65
- endIdx,
66
- );
67
- const after = fileContent.slice(endIdx + LORE_SECTION_END.length);
115
+ // before = everything before the first lore section (start marker not included)
116
+ // section = body of the first section (used by shouldImport and importFromFile)
117
+ // after = everything after the LAST lore section's end marker
118
+ // Any intermediate duplicate sections are discarded.
119
+ const before = fileContent.slice(0, spans[0].markerStart);
120
+ const section = fileContent.slice(spans[0].bodyStart, spans[0].bodyEnd);
121
+ const after = fileContent.slice(spans[spans.length - 1].spanEnd);
122
+
68
123
  return { before, section, after };
69
124
  }
70
125
 
package/src/gradient.ts CHANGED
@@ -876,7 +876,12 @@ function transformInner(input: {
876
876
  expectedInput = messageTokens + overhead + ltmTokens;
877
877
  }
878
878
 
879
- if (effectiveMinLayer === 0 && expectedInput <= maxInput) {
879
+ // When uncalibrated, apply safety multiplier to the layer-0 decision too.
880
+ // chars/3 undercounts by ~1.63x on real sessions — without this, a session
881
+ // estimated at 146K passes layer 0 but actually costs 214K → overflow.
882
+ const layer0Input = calibrated ? expectedInput : expectedInput * UNCALIBRATED_SAFETY;
883
+
884
+ if (effectiveMinLayer === 0 && layer0Input <= maxInput) {
880
885
  // All messages fit — return unmodified to preserve append-only prompt-cache pattern.
881
886
  // Raw messages are strictly better context than lossy distilled summaries.
882
887
  const messageTokens = calibrated