@openviking/openclaw-plugin 2026.9.2 → 2026.9.4
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/shared/recall-core.mjs +60 -0
- package/package.json +1 -1
- package/shared/recall-core.mjs +65 -0
|
@@ -397,6 +397,29 @@ export async function isContextFaceLegacy(path = stateFile("context-face.json"),
|
|
|
397
397
|
export async function markContextFaceLegacy(path = stateFile("context-face.json"), now = Date.now()) {
|
|
398
398
|
await writeJsonFile(path, { legacyUntil: now + LEGACY_CACHE_TTL_MS });
|
|
399
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* A `peer_scope` the server rejects is remembered the same way, but unlike the
|
|
402
|
+
* context face this one is not a silent capability probe: dropping the field
|
|
403
|
+
* widens recall from the caller's own peer to the whole user root, so doctor
|
|
404
|
+
* reads this file back and warns.
|
|
405
|
+
*/
|
|
406
|
+
export function peerScopeMemoPath() {
|
|
407
|
+
return stateFile("peer-scope.json");
|
|
408
|
+
}
|
|
409
|
+
export async function readPeerScopeDowngrade(path = peerScopeMemoPath(), now = Date.now()) {
|
|
410
|
+
const cached = await readJsonFile(path);
|
|
411
|
+
if (!cached?.legacyUntil || Number(cached.legacyUntil) <= now)
|
|
412
|
+
return null;
|
|
413
|
+
return cached;
|
|
414
|
+
}
|
|
415
|
+
export async function markPeerScopeDowngrade(scope, status, path = peerScopeMemoPath(), now = Date.now()) {
|
|
416
|
+
await writeJsonFile(path, {
|
|
417
|
+
legacyUntil: now + LEGACY_CACHE_TTL_MS,
|
|
418
|
+
scope: String(scope || ""),
|
|
419
|
+
status: Number(status) || 0,
|
|
420
|
+
at: now,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
400
423
|
function looksLikeUnknownField(res) {
|
|
401
424
|
const text = JSON.stringify(res?.error ?? res?.result ?? res?.detail ?? "").toLowerCase();
|
|
402
425
|
return text.includes("extra") || text.includes("mode") || text.includes("unexpected");
|
|
@@ -532,7 +555,13 @@ async function recallViaEndpoint(fetchJSON, cfg, query, actorPeerId = "", log =
|
|
|
532
555
|
export async function postRecall(fetchJSON, body, opts = {}) {
|
|
533
556
|
const actorPeerId = opts.actorPeerId || "";
|
|
534
557
|
const log = opts.log || (() => { });
|
|
558
|
+
const memoPath = opts.peerScopeMemoPath;
|
|
535
559
|
const request = { ...body };
|
|
560
|
+
// A remembered downgrade is still a downgrade: recall runs wider than the
|
|
561
|
+
// caller asked for, so the memo doubles as the doctor's evidence.
|
|
562
|
+
if (request.peer_scope && await readPeerScopeDowngrade(memoPath)) {
|
|
563
|
+
delete request.peer_scope;
|
|
564
|
+
}
|
|
536
565
|
const res = await fetchJSON("/api/v1/search/recall", {
|
|
537
566
|
method: "POST",
|
|
538
567
|
body: JSON.stringify(request),
|
|
@@ -540,15 +569,46 @@ export async function postRecall(fetchJSON, body, opts = {}) {
|
|
|
540
569
|
if (!request.peer_scope || (res.status !== 400 && res.status !== 422)) {
|
|
541
570
|
return res;
|
|
542
571
|
}
|
|
572
|
+
// Only an unknown-field rejection means "this server predates peer_scope".
|
|
573
|
+
// Retrying every other 400/422 without it silently widens the search from
|
|
574
|
+
// the caller's own peer to the whole user root.
|
|
575
|
+
if (!looksLikeUnknownField(res)) {
|
|
576
|
+
log("recall_peer_scope_error", { status: res.status || 0 });
|
|
577
|
+
return res;
|
|
578
|
+
}
|
|
543
579
|
const downgraded = { ...request };
|
|
544
580
|
delete downgraded.peer_scope;
|
|
581
|
+
await markPeerScopeDowngrade(String(request.peer_scope), res.status || 0, memoPath);
|
|
545
582
|
log("recall_peer_scope_downgrade", { status: res.status || 0 });
|
|
546
583
|
return fetchJSON("/api/v1/search/recall", {
|
|
547
584
|
method: "POST",
|
|
548
585
|
body: JSON.stringify(downgraded),
|
|
549
586
|
}, { actorPeerId });
|
|
550
587
|
}
|
|
588
|
+
/**
|
|
589
|
+
* Recall, plus whatever is still filed under the peer this workspace used
|
|
590
|
+
* before the identity rule changed.
|
|
591
|
+
*
|
|
592
|
+
* Under the default `peer_scope: "all"` this costs nothing: the server's own
|
|
593
|
+
* sweep of `{user_root}/peers` already reaches the old peer's memories. Under
|
|
594
|
+
* `"actor"` that sweep is off by definition, so the old peer is asked for
|
|
595
|
+
* separately — as itself, which is both cheaper and wider than a bare
|
|
596
|
+
* cross-peer read (that would need the user id, and reaches memories only).
|
|
597
|
+
*/
|
|
551
598
|
export async function buildRecallBlock(fetchJSON, cfg, query, options = {}) {
|
|
599
|
+
const primary = await recallForPeer(fetchJSON, cfg, query, options);
|
|
600
|
+
const legacyPeerId = String(options.legacyPeerId || "").trim();
|
|
601
|
+
const actorPeerId = options.actorPeerId ?? cfg.peerId ?? "";
|
|
602
|
+
if (cfg.recallPeerScope !== "actor" || !legacyPeerId || legacyPeerId === actorPeerId)
|
|
603
|
+
return primary;
|
|
604
|
+
const log = options.log || (() => { });
|
|
605
|
+
const legacy = await recallForPeer(fetchJSON, cfg, query, { ...options, actorPeerId: legacyPeerId });
|
|
606
|
+
if (!legacy)
|
|
607
|
+
return primary;
|
|
608
|
+
log("recall_legacy_peer_hit", { legacyPeerId });
|
|
609
|
+
return primary ? `${primary}\n${legacy}` : legacy;
|
|
610
|
+
}
|
|
611
|
+
async function recallForPeer(fetchJSON, cfg, query, options = {}) {
|
|
552
612
|
const actorPeerId = options.actorPeerId ?? cfg.peerId ?? "";
|
|
553
613
|
const log = options.log || (() => { });
|
|
554
614
|
const trimmed = String(query || "").trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openviking/openclaw-plugin",
|
|
3
|
-
"version": "2026.9.
|
|
3
|
+
"version": "2026.9.4",
|
|
4
4
|
"displayName": "OpenViking",
|
|
5
5
|
"description": "OpenClaw context-engine plugin — OpenViking AI memory, long-term memory, knowledge base, semantic search, RAG, context engine. 长期记忆 · 知识库 · 语义检索 · 上下文引擎",
|
|
6
6
|
"type": "module",
|
package/shared/recall-core.mjs
CHANGED
|
@@ -417,6 +417,31 @@ export async function markContextFaceLegacy(path = stateFile("context-face.json"
|
|
|
417
417
|
await writeJsonFile(path, { legacyUntil: now + LEGACY_CACHE_TTL_MS });
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
/**
|
|
421
|
+
* A `peer_scope` the server rejects is remembered the same way, but unlike the
|
|
422
|
+
* context face this one is not a silent capability probe: dropping the field
|
|
423
|
+
* widens recall from the caller's own peer to the whole user root, so doctor
|
|
424
|
+
* reads this file back and warns.
|
|
425
|
+
*/
|
|
426
|
+
export function peerScopeMemoPath() {
|
|
427
|
+
return stateFile("peer-scope.json");
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export async function readPeerScopeDowngrade(path = peerScopeMemoPath(), now = Date.now()) {
|
|
431
|
+
const cached = await readJsonFile(path);
|
|
432
|
+
if (!cached?.legacyUntil || Number(cached.legacyUntil) <= now) return null;
|
|
433
|
+
return cached;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export async function markPeerScopeDowngrade(scope, status, path = peerScopeMemoPath(), now = Date.now()) {
|
|
437
|
+
await writeJsonFile(path, {
|
|
438
|
+
legacyUntil: now + LEGACY_CACHE_TTL_MS,
|
|
439
|
+
scope: String(scope || ""),
|
|
440
|
+
status: Number(status) || 0,
|
|
441
|
+
at: now,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
420
445
|
function looksLikeUnknownField(res) {
|
|
421
446
|
const text = JSON.stringify(res?.error ?? res?.result ?? res?.detail ?? "").toLowerCase();
|
|
422
447
|
return text.includes("extra") || text.includes("mode") || text.includes("unexpected");
|
|
@@ -558,7 +583,15 @@ async function recallViaEndpoint(fetchJSON, cfg, query, actorPeerId = "", log =
|
|
|
558
583
|
export async function postRecall(fetchJSON, body, opts = {}) {
|
|
559
584
|
const actorPeerId = opts.actorPeerId || "";
|
|
560
585
|
const log = opts.log || (() => {});
|
|
586
|
+
const memoPath = opts.peerScopeMemoPath;
|
|
561
587
|
const request = { ...body };
|
|
588
|
+
|
|
589
|
+
// A remembered downgrade is still a downgrade: recall runs wider than the
|
|
590
|
+
// caller asked for, so the memo doubles as the doctor's evidence.
|
|
591
|
+
if (request.peer_scope && await readPeerScopeDowngrade(memoPath)) {
|
|
592
|
+
delete request.peer_scope;
|
|
593
|
+
}
|
|
594
|
+
|
|
562
595
|
const res = await fetchJSON("/api/v1/search/recall", {
|
|
563
596
|
method: "POST",
|
|
564
597
|
body: JSON.stringify(request),
|
|
@@ -566,9 +599,17 @@ export async function postRecall(fetchJSON, body, opts = {}) {
|
|
|
566
599
|
if (!request.peer_scope || (res.status !== 400 && res.status !== 422)) {
|
|
567
600
|
return res;
|
|
568
601
|
}
|
|
602
|
+
// Only an unknown-field rejection means "this server predates peer_scope".
|
|
603
|
+
// Retrying every other 400/422 without it silently widens the search from
|
|
604
|
+
// the caller's own peer to the whole user root.
|
|
605
|
+
if (!looksLikeUnknownField(res)) {
|
|
606
|
+
log("recall_peer_scope_error", { status: res.status || 0 });
|
|
607
|
+
return res;
|
|
608
|
+
}
|
|
569
609
|
|
|
570
610
|
const downgraded = { ...request };
|
|
571
611
|
delete downgraded.peer_scope;
|
|
612
|
+
await markPeerScopeDowngrade(String(request.peer_scope), res.status || 0, memoPath);
|
|
572
613
|
log("recall_peer_scope_downgrade", { status: res.status || 0 });
|
|
573
614
|
return fetchJSON("/api/v1/search/recall", {
|
|
574
615
|
method: "POST",
|
|
@@ -576,7 +617,31 @@ export async function postRecall(fetchJSON, body, opts = {}) {
|
|
|
576
617
|
}, { actorPeerId });
|
|
577
618
|
}
|
|
578
619
|
|
|
620
|
+
/**
|
|
621
|
+
* Recall, plus whatever is still filed under the peer this workspace used
|
|
622
|
+
* before the identity rule changed.
|
|
623
|
+
*
|
|
624
|
+
* Under the default `peer_scope: "all"` this costs nothing: the server's own
|
|
625
|
+
* sweep of `{user_root}/peers` already reaches the old peer's memories. Under
|
|
626
|
+
* `"actor"` that sweep is off by definition, so the old peer is asked for
|
|
627
|
+
* separately — as itself, which is both cheaper and wider than a bare
|
|
628
|
+
* cross-peer read (that would need the user id, and reaches memories only).
|
|
629
|
+
*/
|
|
579
630
|
export async function buildRecallBlock(fetchJSON, cfg, query, options = {}) {
|
|
631
|
+
const primary = await recallForPeer(fetchJSON, cfg, query, options);
|
|
632
|
+
|
|
633
|
+
const legacyPeerId = String(options.legacyPeerId || "").trim();
|
|
634
|
+
const actorPeerId = options.actorPeerId ?? cfg.peerId ?? "";
|
|
635
|
+
if (cfg.recallPeerScope !== "actor" || !legacyPeerId || legacyPeerId === actorPeerId) return primary;
|
|
636
|
+
|
|
637
|
+
const log = options.log || (() => {});
|
|
638
|
+
const legacy = await recallForPeer(fetchJSON, cfg, query, { ...options, actorPeerId: legacyPeerId });
|
|
639
|
+
if (!legacy) return primary;
|
|
640
|
+
log("recall_legacy_peer_hit", { legacyPeerId });
|
|
641
|
+
return primary ? `${primary}\n${legacy}` : legacy;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function recallForPeer(fetchJSON, cfg, query, options = {}) {
|
|
580
645
|
const actorPeerId = options.actorPeerId ?? cfg.peerId ?? "";
|
|
581
646
|
const log = options.log || (() => {});
|
|
582
647
|
const trimmed = String(query || "").trim();
|