opencode-lore 0.2.4 → 0.2.5

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/gradient.ts +49 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-lore",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
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
@@ -926,6 +926,23 @@ export function estimateMessages(messages: MessageWithParts[]): number {
926
926
  return messages.reduce((sum, m) => sum + estimateMessage(m), 0);
927
927
  }
928
928
 
929
+ // Identify the current agentic turn: the last user message plus all subsequent
930
+ // assistant messages that share its ID as parentID. These messages form an atomic
931
+ // unit — the model must see all of them or it will lose track of its own prior
932
+ // tool calls and re-issue them in an infinite loop.
933
+ function currentTurnStart(messages: MessageWithParts[]): number {
934
+ // Find the last user message
935
+ let lastUserIdx = -1;
936
+ for (let i = messages.length - 1; i >= 0; i--) {
937
+ if (messages[i].info.role === "user") {
938
+ lastUserIdx = i;
939
+ break;
940
+ }
941
+ }
942
+ if (lastUserIdx === -1) return 0; // no user message — treat all as current turn
943
+ return lastUserIdx;
944
+ }
945
+
929
946
  function tryFit(input: {
930
947
  messages: MessageWithParts[];
931
948
  prefix: MessageWithParts[];
@@ -939,32 +956,49 @@ function tryFit(input: {
939
956
  if (input.prefixTokens > input.distilledBudget && input.prefix.length > 0)
940
957
  return null;
941
958
 
942
- // Walk backwards through messages, accumulating tokens within raw budget
943
- let rawTokens = 0;
944
- let cutoff = input.messages.length;
959
+ // Identify the current turn (last user message + all following assistant messages).
960
+ // These are always included — they must never be evicted. If they alone exceed the
961
+ // raw budget, escalate to the next layer (which strips tool outputs to reduce size).
962
+ const turnStart = currentTurnStart(input.messages);
963
+ const currentTurn = input.messages.slice(turnStart);
964
+ const currentTurnTokens = currentTurn.reduce((s, m) => s + estimateMessage(m), 0);
965
+
966
+ if (currentTurnTokens > input.rawBudget) {
967
+ // Current turn alone exceeds budget — can't fit even with everything else dropped.
968
+ // Signal failure so the caller escalates to the next layer (tool-output stripping).
969
+ return null;
970
+ }
971
+
972
+ // Walk backwards through older messages (before the current turn),
973
+ // filling the remaining budget after reserving space for the current turn.
974
+ const olderMessages = input.messages.slice(0, turnStart);
975
+ const remainingBudget = input.rawBudget - currentTurnTokens;
976
+ let olderTokens = 0;
977
+ let cutoff = olderMessages.length; // default: include none of the older messages
945
978
  const protectedTurns = input.protectedTurns ?? 0;
946
- let turns = 0;
947
979
 
948
- for (let i = input.messages.length - 1; i >= 0; i--) {
949
- const msg = input.messages[i];
950
- if (msg.info.role === "user") turns++;
980
+ for (let i = olderMessages.length - 1; i >= 0; i--) {
981
+ const msg = olderMessages[i];
951
982
  const tokens = estimateMessage(msg);
952
- if (rawTokens + tokens > input.rawBudget) {
983
+ if (olderTokens + tokens > remainingBudget) {
953
984
  cutoff = i + 1;
954
985
  break;
955
986
  }
956
- rawTokens += tokens;
987
+ olderTokens += tokens;
957
988
  if (i === 0) cutoff = 0;
958
989
  }
959
990
 
960
- const raw = input.messages.slice(cutoff);
961
- // Must keep at least 1 raw message — otherwise this layer fails
962
- if (!raw.length) return null;
991
+ const rawMessages = [...olderMessages.slice(cutoff), ...currentTurn];
992
+ const rawTokens = olderTokens + currentTurnTokens;
963
993
 
964
- // Apply system-reminder stripping + optional tool output stripping
965
- const processed = raw.map((msg, idx) => {
966
- const fromEnd = raw.length - idx;
994
+ // Apply system-reminder stripping + optional tool output stripping.
995
+ // The current turn (end of rawMessages) is always "protected" — never stripped.
996
+ const currentTurnSet = new Set(currentTurn.map((m) => m.info.id));
997
+ const processed = rawMessages.map((msg, idx) => {
998
+ const fromEnd = rawMessages.length - idx;
999
+ const isCurrentTurn = currentTurnSet.has(msg.info.id);
967
1000
  const isProtected =
1001
+ isCurrentTurn ||
968
1002
  input.strip === "none" ||
969
1003
  (input.strip === "old-tools" && fromEnd <= protectedTurns * 2);
970
1004
  const parts = isProtected