opencode-lore 0.2.1 → 0.2.2

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.2.1",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Three-tier memory architecture for OpenCode — distillation, not summarization",
package/src/gradient.ts CHANGED
@@ -722,8 +722,27 @@ export function transform(input: {
722
722
  const maxInput = contextLimit - outputReserved;
723
723
  const sid = input.sessionID ?? input.messages[0]?.info.sessionID;
724
724
 
725
+ // True when we have real API token data from a previous turn in this session.
726
+ // When false (first turn / session change), chars/4 estimates can undercount by
727
+ // up to 1.8x — so tryFit output must be validated with a safety multiplier before
728
+ // being used, to prevent sending an apparently-fitting window that actually overflows.
729
+ const calibrated = lastKnownInput > 0 && sid === lastKnownSessionID;
730
+
731
+ // On uncalibrated turns, apply this multiplier to tryFit's estimated total to
732
+ // approximate the real token count. 1.5 is conservative but not so aggressive
733
+ // that it forces layer 4 on modestly-sized sessions.
734
+ const UNCALIBRATED_SAFETY = 1.5;
735
+
736
+ // Returns true if the tryFit result is safe to use: either we have calibrated
737
+ // data (exact) or the estimated total * safety factor fits within maxInput.
738
+ function fitsWithSafetyMargin(result: { totalTokens: number } | null): boolean {
739
+ if (!result) return false;
740
+ if (calibrated) return true;
741
+ return result.totalTokens * UNCALIBRATED_SAFETY <= maxInput;
742
+ }
743
+
725
744
  let expectedInput: number;
726
- if (lastKnownInput > 0 && sid === lastKnownSessionID) {
745
+ if (calibrated) {
727
746
  // Exact approach: prior API count + estimate of only the new messages.
728
747
  const newMsgCount = Math.max(0, input.messages.length - lastKnownMessageCount);
729
748
  const newMsgTokens = newMsgCount > 0
@@ -793,7 +812,7 @@ export function transform(input: {
793
812
  rawBudget,
794
813
  strip: "none",
795
814
  });
796
- if (layer1) return { ...layer1, layer: 1, usable, distilledBudget, rawBudget };
815
+ if (fitsWithSafetyMargin(layer1)) return { ...layer1!, layer: 1, usable, distilledBudget, rawBudget };
797
816
  }
798
817
 
799
818
  // Layer 1 didn't fit (or was force-skipped) — reset the raw window cache.
@@ -812,9 +831,9 @@ export function transform(input: {
812
831
  strip: "old-tools",
813
832
  protectedTurns: 2,
814
833
  });
815
- if (layer2) {
834
+ if (fitsWithSafetyMargin(layer2)) {
816
835
  urgentDistillation = true;
817
- return { ...layer2, layer: 2, usable, distilledBudget, rawBudget };
836
+ return { ...layer2!, layer: 2, usable, distilledBudget, rawBudget };
818
837
  }
819
838
  }
820
839
 
@@ -833,9 +852,9 @@ export function transform(input: {
833
852
  rawBudget: Math.floor(usable * 0.55),
834
853
  strip: "all-tools",
835
854
  });
836
- if (layer3) {
855
+ if (fitsWithSafetyMargin(layer3)) {
837
856
  urgentDistillation = true;
838
- return { ...layer3, layer: 3, usable, distilledBudget, rawBudget };
857
+ return { ...layer3!, layer: 3, usable, distilledBudget, rawBudget };
839
858
  }
840
859
 
841
860
  // Layer 4: Emergency — last 2 distillations, last 3 raw messages with tool parts intact.
package/src/index.ts CHANGED
@@ -389,12 +389,13 @@ export const LorePlugin: Plugin = async (ctx) => {
389
389
  // Layer 0 means all messages fit within the context budget — leave them alone
390
390
  // so the append-only sequence stays intact for prompt caching.
391
391
  if (result.layer > 0) {
392
+ // The API requires the conversation to end with a user message.
393
+ // Always drop trailing non-user messages — even assistant messages with
394
+ // tool parts. A hard API error is worse than the model re-invoking a tool.
392
395
  while (
393
396
  result.messages.length > 0 &&
394
397
  result.messages.at(-1)!.info.role !== "user"
395
398
  ) {
396
- const last = result.messages.at(-1)!;
397
- if (last.parts.some((p) => p.type === "tool")) break;
398
399
  const dropped = result.messages.pop()!;
399
400
  console.error(
400
401
  "[lore] WARN: dropping trailing",