@eleboucher/opencode-memini 0.6.10 → 0.6.11
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/memini.js +46 -3
- package/package.json +1 -1
package/memini.js
CHANGED
|
@@ -779,8 +779,18 @@ export const MeminiPlugin = async ({ client, worktree, directory }, options) =>
|
|
|
779
779
|
}
|
|
780
780
|
}
|
|
781
781
|
// Assistant message ids already captured, so repeated session.idle events for
|
|
782
|
-
// the same turn don't write duplicates.
|
|
782
|
+
// the same turn don't write duplicates. Repeats only concern recent turns,
|
|
783
|
+
// so cap the window instead of growing one entry per turn forever.
|
|
783
784
|
const captured = new Set();
|
|
785
|
+
const MAX_CAPTURED = 200;
|
|
786
|
+
const rememberCaptured = (id) => {
|
|
787
|
+
captured.add(id);
|
|
788
|
+
while (captured.size > MAX_CAPTURED) {
|
|
789
|
+
const oldest = captured.values().next().value;
|
|
790
|
+
if (oldest === undefined) break;
|
|
791
|
+
captured.delete(oldest);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
784
794
|
// boundedPut inserts key -> value and evicts the oldest entries, so a
|
|
785
795
|
// long-lived host can't grow a per-session map without limit.
|
|
786
796
|
const MAX_TRACKED_SESSIONS = 200;
|
|
@@ -795,7 +805,10 @@ export const MeminiPlugin = async ({ client, worktree, directory }, options) =>
|
|
|
795
805
|
// Memory ids each session has already been shown (mirrors the pi plugin):
|
|
796
806
|
// the injected synthetic part is persisted into the session, so re-injecting
|
|
797
807
|
// an unchanged match every turn stacks identical blocks in the context.
|
|
808
|
+
// The inner cap keeps a stable session — which never ages out of the outer
|
|
809
|
+
// map — from growing its Set for the process lifetime.
|
|
798
810
|
const injectedBySession = new Map();
|
|
811
|
+
const MAX_INJECTED_PER_SESSION = 200;
|
|
799
812
|
const rememberInjected = (session, ids) => {
|
|
800
813
|
let seen = injectedBySession.get(session);
|
|
801
814
|
if (!seen) {
|
|
@@ -803,11 +816,38 @@ export const MeminiPlugin = async ({ client, worktree, directory }, options) =>
|
|
|
803
816
|
boundedPut(injectedBySession, session, seen);
|
|
804
817
|
}
|
|
805
818
|
for (const id of ids) if (id) seen.add(id);
|
|
819
|
+
while (seen.size > MAX_INJECTED_PER_SESSION) {
|
|
820
|
+
const oldest = seen.values().next().value;
|
|
821
|
+
if (oldest === undefined) break;
|
|
822
|
+
seen.delete(oldest);
|
|
823
|
+
}
|
|
806
824
|
};
|
|
807
825
|
// Recall results that arrived after the injection budget expired, keyed by
|
|
808
826
|
// session and injected on that session's next chat.message. Latest-replace:
|
|
809
827
|
// a second late recall for the same session supersedes the first.
|
|
810
828
|
const pendingBySession = new Map();
|
|
829
|
+
// /v1/search drops exclude_ids before ranking and the limit, so an
|
|
830
|
+
// already-shown hit frees its slot for the next-best match. Older servers
|
|
831
|
+
// 400 on the unknown field: when a request carrying it fails and the retry
|
|
832
|
+
// without it succeeds, stop sending it. The client-side filter stays.
|
|
833
|
+
let serverExcludeIds = true;
|
|
834
|
+
const searchExcluding = async (body, excludeIds) => {
|
|
835
|
+
if (!serverExcludeIds || excludeIds.length === 0) {
|
|
836
|
+
return rest.postJson("/v1/search", body);
|
|
837
|
+
}
|
|
838
|
+
try {
|
|
839
|
+
const result = await rest.postJson("/v1/search", { ...body, exclude_ids: excludeIds });
|
|
840
|
+
if (result !== null) return result;
|
|
841
|
+
} catch {
|
|
842
|
+
// With fallback_on_error=false the 400 arrives as a throw, not null.
|
|
843
|
+
}
|
|
844
|
+
const retry = await rest.postJson("/v1/search", body);
|
|
845
|
+
if (retry !== null) {
|
|
846
|
+
serverExcludeIds = false;
|
|
847
|
+
log.warn("memini: server does not accept exclude_ids; using client-side dedupe only");
|
|
848
|
+
}
|
|
849
|
+
return retry;
|
|
850
|
+
};
|
|
811
851
|
|
|
812
852
|
// opencode runs chat.message via an unguarded Effect.promise (a throw aborts the
|
|
813
853
|
// turn) and dispatches event hooks fire-and-forget, so a hook must never reject:
|
|
@@ -880,12 +920,15 @@ export const MeminiPlugin = async ({ client, worktree, directory }, options) =>
|
|
|
880
920
|
// the Claude Code plugin's pre-tool-use hook uses; client-side re-filter
|
|
881
921
|
// is a belt-and-braces guard against score-normalization edge cases.
|
|
882
922
|
if (cfg.recall_min_score > 0) body.min_score = cfg.recall_min_score;
|
|
923
|
+
// Already-shown ids go along as exclude_ids so a suppressed hit doesn't
|
|
924
|
+
// waste a recall_limit slot.
|
|
925
|
+
const excludeIds = sessionID ? [...(injectedBySession.get(sessionID) ?? [])] : [];
|
|
883
926
|
// opencode awaits this hook before the model sees the message, so the
|
|
884
927
|
// turn only waits recall_budget_ms for the search; the fetch itself keeps
|
|
885
928
|
// cfg.timeout_ms as its bound and runs on in the background. A slow or
|
|
886
929
|
// unreachable memini degrades to "no memories this turn" instead of a
|
|
887
930
|
// frozen turn, and late results carry over to the session's next message.
|
|
888
|
-
const fetchPromise =
|
|
931
|
+
const fetchPromise = searchExcluding(body, excludeIds);
|
|
889
932
|
// Once the budget expires nothing awaits this promise, and with
|
|
890
933
|
// fallback_on_error off postJson rethrows — catch here or a late
|
|
891
934
|
// rejection surfaces as an unhandled rejection in the host.
|
|
@@ -999,7 +1042,7 @@ export const MeminiPlugin = async ({ client, worktree, directory }, options) =>
|
|
|
999
1042
|
tags: ["opencode"],
|
|
1000
1043
|
metadata,
|
|
1001
1044
|
});
|
|
1002
|
-
if (stored !== null && assistantID)
|
|
1045
|
+
if (stored !== null && assistantID) rememberCaptured(assistantID);
|
|
1003
1046
|
}),
|
|
1004
1047
|
};
|
|
1005
1048
|
};
|