@atrib/recall 0.12.14 → 0.12.16
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/index.d.ts +37 -0
- package/dist/index.js +563 -550
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -116,6 +116,7 @@ export const ATRIB_RECALL_NOISE_FLOOR = parseFloat(process.env.ATRIB_RECALL_NOIS
|
|
|
116
116
|
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
117
117
|
import { homedir } from 'node:os';
|
|
118
118
|
import { join } from 'node:path';
|
|
119
|
+
import { pathToFileURL } from 'node:url';
|
|
119
120
|
import { z } from 'zod';
|
|
120
121
|
import { aggregateAnnotationsByRecord, aggregateRevisionsByRecord, discoverLoaded, } from './aggregations.js';
|
|
121
122
|
import { recencyScore, importanceScore, parkScore, buildBM25Index, bm25Score, tokenize, indexableTokensForRecord, } from './scoring.js';
|
|
@@ -188,7 +189,7 @@ export function loadRecords(path) {
|
|
|
188
189
|
export function loadRecordsFromDir(dir) {
|
|
189
190
|
if (!existsSync(dir))
|
|
190
191
|
return { records: [], files: [] };
|
|
191
|
-
let entries
|
|
192
|
+
let entries;
|
|
192
193
|
try {
|
|
193
194
|
entries = readdirSync(dir).filter((name) => name.endsWith('.jsonl'));
|
|
194
195
|
}
|
|
@@ -322,7 +323,7 @@ function rankByCausalDistance(filtered, all, rankAnchor) {
|
|
|
322
323
|
}
|
|
323
324
|
async function annotateVerification(loaded, annotationsByRecord, revisionsByRecord) {
|
|
324
325
|
return Promise.all(loaded.map(async (lr) => {
|
|
325
|
-
let ok
|
|
326
|
+
let ok;
|
|
326
327
|
try {
|
|
327
328
|
ok = await verifyRecord(lr.record);
|
|
328
329
|
}
|
|
@@ -623,10 +624,6 @@ function readPackageVersion() {
|
|
|
623
624
|
return '0.0.0';
|
|
624
625
|
}
|
|
625
626
|
}
|
|
626
|
-
const server = new McpServer({
|
|
627
|
-
name: 'atrib-recall',
|
|
628
|
-
version: readPackageVersion(),
|
|
629
|
-
});
|
|
630
627
|
// The recall semantic surface (as defined in the public protocol specification).
|
|
631
628
|
// Eight distinct MCP tools: recall_my_attribution_history is the base
|
|
632
629
|
// filter-and-page tool; recall_annotations + recall_revisions return
|
|
@@ -634,565 +631,581 @@ const server = new McpServer({
|
|
|
634
631
|
// record_hash; recall_walk traverses the local Layer 1 derived graph;
|
|
635
632
|
// recall_by_content runs BM25 free-form retrieval; recall_session_chain,
|
|
636
633
|
// recall_orphans, and recall_by_signer cover common agent lookup shapes.
|
|
637
|
-
server
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
'
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
},
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
content: [
|
|
761
|
-
{
|
|
762
|
-
type: 'text',
|
|
763
|
-
text: JSON.stringify(result, null, 2),
|
|
764
|
-
},
|
|
765
|
-
],
|
|
766
|
-
};
|
|
767
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
768
|
-
// ─── Layer 1 sibling tools ───
|
|
769
|
-
// Sibling tools expose common agent lookup shapes beyond the base
|
|
770
|
-
// filter-and-page tool.
|
|
771
|
-
server.registerTool('recall_walk', {
|
|
772
|
-
description: 'Walk the local derived graph from a starting record_hash. Returns records reachable via the requested edge types up to the given hop depth, ordered by ascending weighted distance. Layer 1 covers four edge types: CHAIN_PRECEDES (weight 1), INFORMED_BY (weight 1), ANNOTATES (weight 2), REVISES (weight 2). SESSION_PRECEDES, SESSION_PARALLEL, CONVERGES_ON, CROSS_SESSION, and PROVENANCE_OF are deferred to subsequent releases. Useful for tracing the local causal neighborhood of a record before re-attempting a similar action.',
|
|
773
|
-
inputSchema: {
|
|
774
|
-
from_record_hash: z
|
|
775
|
-
.string()
|
|
776
|
-
.describe('Starting record hash (sha256:<64-hex>). The walk begins here and expands through the local derived graph.'),
|
|
777
|
-
edge_types: z
|
|
778
|
-
.array(z.enum(['CHAIN_PRECEDES', 'INFORMED_BY', 'ANNOTATES', 'REVISES']))
|
|
779
|
-
.optional()
|
|
780
|
-
.describe('Optional list of Layer 1 edge types to follow. Default: all four. Unknown values are rejected by the schema.'),
|
|
781
|
-
depth: z
|
|
782
|
-
.number()
|
|
783
|
-
.optional()
|
|
784
|
-
.describe('Maximum hop count (NOT cumulative weight). Default 3. Higher values may return many records; paginate downstream if needed.'),
|
|
785
|
-
},
|
|
786
|
-
}, async (args) => logReadPrimitiveCall('recall_walk', args, async () => {
|
|
787
|
-
const { loaded } = discoverLoaded();
|
|
788
|
-
const graph = buildLocalGraph(loaded);
|
|
789
|
-
const edgeTypes = args.edge_types ? new Set(args.edge_types) : undefined;
|
|
790
|
-
const depth = typeof args.depth === 'number' ? args.depth : 3;
|
|
791
|
-
const walk = walkFrom(graph, args.from_record_hash, edgeTypes, depth);
|
|
792
|
-
// Layer 1 v2 legibility: join walked hashes back to their loaded
|
|
793
|
-
// records so each step in the walk carries a readable label
|
|
794
|
-
// instead of just a hash + distance. Builds the index once for
|
|
795
|
-
// O(N) lookup across the walk; for typical walks (depth=3, k<50)
|
|
796
|
-
// this is fast.
|
|
797
|
-
const byHash = new Map(loaded.map((lr) => [lr.record_hash, lr]));
|
|
798
|
-
const annotationsByRecord = aggregateAnnotationsByRecord(loaded);
|
|
799
|
-
const now = Date.now();
|
|
800
|
-
const enriched = walk.map((step) => {
|
|
801
|
-
// walk derives from graph (built from `loaded`); byHash always has a hit.
|
|
802
|
-
const lr = byHash.get(step.record_hash);
|
|
803
|
-
const ann = annotationsByRecord.get(step.record_hash);
|
|
634
|
+
export function registerAtribRecallTools(server) {
|
|
635
|
+
server.registerTool('recall_my_attribution_history', {
|
|
636
|
+
description: "Return signed atrib records from the local mirror. The agent's own past, with each record's " +
|
|
637
|
+
'Ed25519 signature verified locally. By default the response is compact (no signature bytes) and ' +
|
|
638
|
+
'includes only records that passed signature verification; both can be opted out of with ' +
|
|
639
|
+
'compact=false and include_unverified=true respectively. Local signature verification proves ' +
|
|
640
|
+
'"this record was signed by that creator_key"; it does NOT prove log inclusion (fetch a log ' +
|
|
641
|
+
'inclusion proof to confirm). Filter by context_id (specific trace), event_type ' +
|
|
642
|
+
'(tool_call|transaction|observation|annotation|revision|directory_anchor or a full URI), content_id (specific tool on specific server), tool_name (disclosed ' +
|
|
643
|
+
'name per §8.2), or args_hash (canonical-args commitment per §8.3). Filters are AND-combined; ' +
|
|
644
|
+
'omit all of them for cross-trace history. Results are sorted newest-first. Pagination uses ' +
|
|
645
|
+
'offset; new records appended between calls invalidate offset stability. See the ' +
|
|
646
|
+
'pagination_caveat in the response. The filtered_out_by_verification field reports how many ' +
|
|
647
|
+
'records were dropped due to signature failures (always 0 when include_unverified=true).',
|
|
648
|
+
inputSchema: {
|
|
649
|
+
context_id: z
|
|
650
|
+
.string()
|
|
651
|
+
.optional()
|
|
652
|
+
.describe('Optional trace identifier (32 hex chars). Limits results to records signed within this trace. ' +
|
|
653
|
+
'Omit for cross-trace recall.'),
|
|
654
|
+
creator_key: z
|
|
655
|
+
.string()
|
|
656
|
+
.optional()
|
|
657
|
+
.describe('Optional exact match on record.creator_key (base64url-encoded Ed25519 public key, 43 chars). ' +
|
|
658
|
+
'Filters the local mirror to records signed by this specific creator. Omit to see all signers ' +
|
|
659
|
+
"present in the mirror; the tool name says 'my attribution history' but the mirror may contain " +
|
|
660
|
+
'records from other creators when multi-agent flows ship records into a shared mirror. Use ' +
|
|
661
|
+
'your own creator_key (resolvable from the @atrib/cli key-show output, or `getPublicKey()` ' +
|
|
662
|
+
'called on your seed) when you want to scope strictly to your own past.'),
|
|
663
|
+
event_type: EventTypeFilterSchema.optional().describe('Optional filter to a single event kind. Accepts atrib shorthand aliases ' +
|
|
664
|
+
'(tool_call, transaction, observation, annotation, revision, directory_anchor) ' +
|
|
665
|
+
'or a full event_type URI. Most calls leave this unset.'),
|
|
666
|
+
content_id: z
|
|
667
|
+
.string()
|
|
668
|
+
.optional()
|
|
669
|
+
.describe('Optional exact match on record.content_id (sha256:<64-hex>). Per spec §1.2.2, content_id ' +
|
|
670
|
+
'is sha256(serverUrl + ":" + toolName), so filtering groups all records emitted by the same ' +
|
|
671
|
+
'tool on the same MCP server. Coarser than tool_name (different servers, same name -> ' +
|
|
672
|
+
'different content_id).'),
|
|
673
|
+
tool_name: z
|
|
674
|
+
.string()
|
|
675
|
+
.optional()
|
|
676
|
+
.describe('Optional exact match on the §8.2 disclosed tool_name. Records that did NOT opt in to ' +
|
|
677
|
+
'tool-name disclosure (the §8.1 default posture) carry no tool_name field and are excluded ' +
|
|
678
|
+
'from results when this filter is set.'),
|
|
679
|
+
args_hash: z
|
|
680
|
+
.string()
|
|
681
|
+
.optional()
|
|
682
|
+
.describe('Optional exact match on record.args_hash (sha256:<64-hex>). Per spec §8.3, args_hash commits ' +
|
|
683
|
+
'to canonical args bytes (salted or plain; both forms hash identically on the wire). Most ' +
|
|
684
|
+
'useful for replay detection or agent-side keyed lookup over a normalized probe hash.'),
|
|
685
|
+
limit: z.number().optional().describe('Page size, default 10, max 200.'),
|
|
686
|
+
offset: z
|
|
687
|
+
.number()
|
|
688
|
+
.optional()
|
|
689
|
+
.describe('Pagination offset, default 0. Note: not stable when new records land between calls - see ' +
|
|
690
|
+
'pagination_caveat in the response.'),
|
|
691
|
+
compact: z
|
|
692
|
+
.boolean()
|
|
693
|
+
.optional()
|
|
694
|
+
.describe('Default true. When true, omit signature/content_id/chain_root/spec_version fields. ' +
|
|
695
|
+
'signature_verified is still included. Set to false (or use the equivalent verbose=true) ' +
|
|
696
|
+
'when you need the full record bytes for re-verification or downstream processing.'),
|
|
697
|
+
include_unverified: z
|
|
698
|
+
.boolean()
|
|
699
|
+
.optional()
|
|
700
|
+
.describe('Default false. When false, records with signature_verified=false are dropped from the ' +
|
|
701
|
+
'response (their count is reported in filtered_out_by_verification). Set to true to ' +
|
|
702
|
+
'include them - useful when investigating tampered or partial mirror state.'),
|
|
703
|
+
// Layer 1 semantic filters. These are enforced in the handler below and
|
|
704
|
+
// share the same response metadata path as the base filters.
|
|
705
|
+
min_importance: z
|
|
706
|
+
.enum(['critical', 'high', 'medium', 'low', 'noise'])
|
|
707
|
+
.optional()
|
|
708
|
+
.describe('Filter to records whose maximum annotation importance is at least this level. Annotation ' +
|
|
709
|
+
'importance comes from annotation records pointing at the record. Records with no ' +
|
|
710
|
+
'annotations at all are excluded when this filter is set.'),
|
|
711
|
+
topic_tags: z
|
|
712
|
+
.array(z.string())
|
|
713
|
+
.optional()
|
|
714
|
+
.describe('OR-match against annotation topic tags. Records are kept if at least one annotation pointing ' +
|
|
715
|
+
'at them carries at least one of the listed topics. Records with no annotations or no ' +
|
|
716
|
+
'topic overlap are excluded.'),
|
|
717
|
+
include_revised: z
|
|
718
|
+
.boolean()
|
|
719
|
+
.optional()
|
|
720
|
+
.describe('Default false: revised records remain visible with superseded_by populated. Set true to hide ' +
|
|
721
|
+
'records that have been superseded by a revision record (revises field equals this record).'),
|
|
722
|
+
min_signers: z
|
|
723
|
+
.number()
|
|
724
|
+
.optional()
|
|
725
|
+
.describe('Minimum count of distinct signers. Transaction records carry a signers[] array (cross- ' +
|
|
726
|
+
'attestation); the count is its length. Non-transaction records have a single signature; ' +
|
|
727
|
+
'their count is 1. Records below the threshold are excluded.'),
|
|
728
|
+
rank_by: z
|
|
729
|
+
.enum(['timestamp', 'relevance', 'causal_distance'])
|
|
730
|
+
.optional()
|
|
731
|
+
.describe('Result ordering. timestamp (default): newest first. relevance: Park et al. weighted-sum ' +
|
|
732
|
+
'scoring over recency + annotation-derived importance + BM25 relevance against rank_anchor ' +
|
|
733
|
+
'(treated as a free-form query when not a record_hash; otherwise relevance component is 0). ' +
|
|
734
|
+
'causal_distance: BFS shortest path in the local derived graph from rank_anchor (a record_hash). ' +
|
|
735
|
+
'Records unreachable from the anchor sort to the end.'),
|
|
736
|
+
rank_anchor: z
|
|
737
|
+
.string()
|
|
738
|
+
.optional()
|
|
739
|
+
.describe('Anchor for non-timestamp rank_by modes. For rank_by=relevance: free-form text query for the ' +
|
|
740
|
+
'BM25 component (matched against annotation summary + topics of each candidate). For ' +
|
|
741
|
+
'rank_by=causal_distance: record_hash to BFS from (sha256:<64-hex>); falls back to timestamp ' +
|
|
742
|
+
'newest-first when not a valid record_hash.'),
|
|
743
|
+
toc: z
|
|
744
|
+
.boolean()
|
|
745
|
+
.optional()
|
|
746
|
+
.describe('Default false. When true, each returned record is the table-of-contents entry shape ' +
|
|
747
|
+
'(record_hash, tool_name, summary, importance, topic_tags, timestamp, superseded_by) at ' +
|
|
748
|
+
'~40-80 tokens per entry. Designed for SessionStart auto-injected scaffold and any other ' +
|
|
749
|
+
'cheap-to-scan candidate set the agent expands on demand via recall(content_id=...) or ' +
|
|
750
|
+
'recall_walk.'),
|
|
751
|
+
},
|
|
752
|
+
}, async (args) => logReadPrimitiveCall('recall_my_attribution_history', args, async () => {
|
|
753
|
+
// All Layer 1 surface parameters are enforced by recall() above:
|
|
754
|
+
// min_importance, topic_tags, include_revised, min_signers,
|
|
755
|
+
// rank_by, rank_anchor, and toc.
|
|
756
|
+
const result = await recall(args);
|
|
804
757
|
return {
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
age: formatAge(lr.record.timestamp, now),
|
|
758
|
+
content: [
|
|
759
|
+
{
|
|
760
|
+
type: 'text',
|
|
761
|
+
text: JSON.stringify(result, null, 2),
|
|
762
|
+
},
|
|
763
|
+
],
|
|
812
764
|
};
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
765
|
+
}, extractRecordHashFieldsFromMcpResult));
|
|
766
|
+
// ─── Layer 1 sibling tools ───
|
|
767
|
+
// Sibling tools expose common agent lookup shapes beyond the base
|
|
768
|
+
// filter-and-page tool.
|
|
769
|
+
server.registerTool('recall_walk', {
|
|
770
|
+
description: 'Walk the local derived graph from a starting record_hash. Returns records reachable via the requested edge types up to the given hop depth, ordered by ascending weighted distance. Layer 1 covers four edge types: CHAIN_PRECEDES (weight 1), INFORMED_BY (weight 1), ANNOTATES (weight 2), REVISES (weight 2). SESSION_PRECEDES, SESSION_PARALLEL, CONVERGES_ON, CROSS_SESSION, and PROVENANCE_OF are deferred to subsequent releases. Useful for tracing the local causal neighborhood of a record before re-attempting a similar action.',
|
|
771
|
+
inputSchema: {
|
|
772
|
+
from_record_hash: z
|
|
773
|
+
.string()
|
|
774
|
+
.describe('Starting record hash (sha256:<64-hex>). The walk begins here and expands through the local derived graph.'),
|
|
775
|
+
edge_types: z
|
|
776
|
+
.array(z.enum(['CHAIN_PRECEDES', 'INFORMED_BY', 'ANNOTATES', 'REVISES']))
|
|
777
|
+
.optional()
|
|
778
|
+
.describe('Optional list of Layer 1 edge types to follow. Default: all four. Unknown values are rejected by the schema.'),
|
|
779
|
+
depth: z
|
|
780
|
+
.number()
|
|
781
|
+
.optional()
|
|
782
|
+
.describe('Maximum hop count (NOT cumulative weight). Default 3. Higher values may return many records; paginate downstream if needed.'),
|
|
783
|
+
},
|
|
784
|
+
}, async (args) => logReadPrimitiveCall('recall_walk', args, async () => {
|
|
785
|
+
const { loaded } = discoverLoaded();
|
|
786
|
+
const graph = buildLocalGraph(loaded);
|
|
787
|
+
const edgeTypes = args.edge_types ? new Set(args.edge_types) : undefined;
|
|
788
|
+
const depth = typeof args.depth === 'number' ? args.depth : 3;
|
|
789
|
+
const walk = walkFrom(graph, args.from_record_hash, edgeTypes, depth);
|
|
790
|
+
// Layer 1 v2 legibility: join walked hashes back to their loaded
|
|
791
|
+
// records so each step in the walk carries a readable label
|
|
792
|
+
// instead of just a hash + distance. Builds the index once for
|
|
793
|
+
// O(N) lookup across the walk; for typical walks (depth=3, k<50)
|
|
794
|
+
// this is fast.
|
|
795
|
+
const byHash = new Map(loaded.map((lr) => [lr.record_hash, lr]));
|
|
796
|
+
const annotationsByRecord = aggregateAnnotationsByRecord(loaded);
|
|
797
|
+
const now = Date.now();
|
|
798
|
+
const enriched = walk.map((step) => {
|
|
799
|
+
// walk derives from graph (built from `loaded`); byHash always has a hit.
|
|
800
|
+
const lr = byHash.get(step.record_hash);
|
|
801
|
+
const ann = annotationsByRecord.get(step.record_hash);
|
|
802
|
+
return {
|
|
803
|
+
record_hash: step.record_hash,
|
|
804
|
+
distance: step.distance,
|
|
805
|
+
event_type: lr.record.event_type,
|
|
806
|
+
timestamp: lr.record.timestamp,
|
|
807
|
+
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
808
|
+
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
809
|
+
age: formatAge(lr.record.timestamp, now),
|
|
810
|
+
};
|
|
811
|
+
});
|
|
812
|
+
return {
|
|
813
|
+
content: [
|
|
814
|
+
{
|
|
815
|
+
type: 'text',
|
|
816
|
+
text: JSON.stringify({
|
|
817
|
+
from_record_hash: args.from_record_hash,
|
|
818
|
+
edge_types: args.edge_types ?? [
|
|
819
|
+
'CHAIN_PRECEDES',
|
|
820
|
+
'INFORMED_BY',
|
|
821
|
+
'ANNOTATES',
|
|
822
|
+
'REVISES',
|
|
823
|
+
],
|
|
824
|
+
depth,
|
|
825
|
+
count: enriched.length,
|
|
826
|
+
walk: enriched,
|
|
827
|
+
}, null, 2),
|
|
828
|
+
},
|
|
829
|
+
],
|
|
830
|
+
};
|
|
831
|
+
}, extractRecordHashFieldsFromMcpResult));
|
|
832
|
+
server.registerTool('recall_annotations', {
|
|
833
|
+
description: "Return the aggregated annotation summary for a record: maximum annotation importance across all D058 annotation records pointing at it, the union of their topic tags, and the most recent summary string. Useful for surfacing the agent's prior critique on a record before re-attempting a similar action. Returns null annotations field when no annotation points at the record.",
|
|
834
|
+
inputSchema: {
|
|
835
|
+
record_hash: z
|
|
836
|
+
.string()
|
|
837
|
+
.describe('Record hash (sha256:<64-hex>) of the record whose annotations should be retrieved. Annotations are D058 records whose signed annotates field equals this hash.'),
|
|
838
|
+
},
|
|
839
|
+
}, async (args) => logReadPrimitiveCall('recall_annotations', args, async () => {
|
|
840
|
+
const { loaded } = discoverLoaded();
|
|
841
|
+
const annotationsByRecord = aggregateAnnotationsByRecord(loaded);
|
|
842
|
+
const summary = annotationsByRecord.get(args.record_hash) ?? null;
|
|
843
|
+
return {
|
|
844
|
+
content: [
|
|
845
|
+
{
|
|
846
|
+
type: 'text',
|
|
847
|
+
text: JSON.stringify({ record_hash: args.record_hash, annotations: summary }, null, 2),
|
|
848
|
+
},
|
|
849
|
+
],
|
|
850
|
+
};
|
|
851
|
+
}, extractRecordHashFieldsFromMcpResult));
|
|
852
|
+
server.registerTool('recall_revisions', {
|
|
853
|
+
description: "Return the D059 revision chain for a record, with per-entry content + sibling-fan-out awareness. Walks revises edges forward from the given record_hash, surfacing each revision in turn. Each entry carries the revision's record_hash, timestamp, and content (`new_position`, `reason`, `importance`) so the agent can read the chain inline without follow-up recall calls per entry. When more than one revision targets the same record, the chain follows the first-by-timestamp branch and lists the other branch heads as `sibling_hashes` on that entry so the agent learns about parallel revision branches (common in multi-agent flows). Useful for checking whether a position the agent previously held has been revised before acting on it. Returns an empty chain when no revision points at the record.",
|
|
854
|
+
inputSchema: {
|
|
855
|
+
record_hash: z
|
|
856
|
+
.string()
|
|
857
|
+
.describe('Record hash (sha256:<64-hex>) of the record whose revision chain should be retrieved. Revisions are D059 records whose signed revises field equals this hash (or chain back to it).'),
|
|
858
|
+
},
|
|
859
|
+
}, async (args) => logReadPrimitiveCall('recall_revisions', args, async () => {
|
|
860
|
+
const { loaded } = discoverLoaded();
|
|
861
|
+
const byHash = new Map();
|
|
862
|
+
for (const lr of loaded)
|
|
863
|
+
byHash.set(lr.record_hash, lr);
|
|
864
|
+
const revisionsByRecord = aggregateRevisionsByRecord(loaded);
|
|
865
|
+
const chain = [];
|
|
866
|
+
const seen = new Set();
|
|
867
|
+
let current = args.record_hash;
|
|
868
|
+
while (!seen.has(current)) {
|
|
869
|
+
seen.add(current);
|
|
870
|
+
const next = revisionsByRecord.get(current);
|
|
871
|
+
if (!next || next.length === 0)
|
|
872
|
+
break;
|
|
873
|
+
// Each entry in the map's value array is a revision pointing at
|
|
874
|
+
// `current`. The chain follows the first-by-timestamp revision;
|
|
875
|
+
// the remaining entries are surfaced as `sibling_hashes` so the
|
|
876
|
+
// agent learns that branches exist without the chain shape
|
|
877
|
+
// having to explode into a tree.
|
|
878
|
+
const revHash = next[0];
|
|
879
|
+
const siblings = next.slice(1);
|
|
880
|
+
const revLr = byHash.get(revHash);
|
|
881
|
+
const entry = { record_hash: revHash };
|
|
882
|
+
if (revLr) {
|
|
883
|
+
entry.timestamp = revLr.record.timestamp;
|
|
884
|
+
const c = revLr.content;
|
|
885
|
+
if (c && typeof c === 'object' && !Array.isArray(c)) {
|
|
886
|
+
const cObj = c;
|
|
887
|
+
if (typeof cObj.new_position === 'string')
|
|
888
|
+
entry.new_position = cObj.new_position;
|
|
889
|
+
if (typeof cObj.reason === 'string')
|
|
890
|
+
entry.reason = cObj.reason;
|
|
891
|
+
if (typeof cObj.importance === 'string')
|
|
892
|
+
entry.importance = cObj.importance;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (siblings.length > 0) {
|
|
896
|
+
entry.sibling_hashes = siblings;
|
|
895
897
|
}
|
|
898
|
+
chain.push(entry);
|
|
899
|
+
current = revHash;
|
|
896
900
|
}
|
|
897
|
-
|
|
898
|
-
|
|
901
|
+
return {
|
|
902
|
+
content: [
|
|
903
|
+
{
|
|
904
|
+
type: 'text',
|
|
905
|
+
text: JSON.stringify({ record_hash: args.record_hash, revision_chain: chain }, null, 2),
|
|
906
|
+
},
|
|
907
|
+
],
|
|
908
|
+
};
|
|
909
|
+
}, extractRecordHashFieldsFromMcpResult));
|
|
910
|
+
server.registerTool('recall_by_content', {
|
|
911
|
+
description: "Free-form text search over the agent's signed past. Returns top-k records by hybrid retrieval: BM25 over each record's per-event_type indexable text (observation `what + why_noted + topics`; tool_call `tool_name + args + result`; annotation `summary + topics`; revision `prior_position + new_position + reason + topics`; transaction counterparty + memo; directory_anchor tree_root; extension URIs via generic recursive string-walk per D086) plus the annotation summary + topics when present as a curator-quality lift. Reranked by Park et al. weighted-sum scoring with annotation-derived importance and recency signals; BM25 contribution clamped to [0, 1] so the documented Park-component bound is honored. Layer 2 (sqlite-vec sidecar, separate ship) extends with embedding similarity. Useful when the agent has no specific filter and needs to ask 'what do I know about X?'.",
|
|
912
|
+
inputSchema: {
|
|
913
|
+
query: z
|
|
914
|
+
.string()
|
|
915
|
+
.describe("Free-form text query. Matches against each record's per-event_type content plus annotation summary and topics via BM25. Records with no indexable text contribute no relevance signal and only surface through the recency or importance fallback."),
|
|
916
|
+
k: z
|
|
917
|
+
.number()
|
|
918
|
+
.optional()
|
|
919
|
+
.describe('Top-k results to return (default 10, max 50). Final ordering uses Park et al. weighted-sum scoring: alpha*recency + beta*importance + gamma*BM25_relevance. Weights are tunable via ATRIB_RECALL_ALPHA/BETA/GAMMA env vars.'),
|
|
920
|
+
},
|
|
921
|
+
}, async (args) => logReadPrimitiveCall('recall_by_content', args, async () => {
|
|
922
|
+
const { loaded } = discoverLoaded();
|
|
923
|
+
const annotationsByRecord = aggregateAnnotationsByRecord(loaded);
|
|
924
|
+
const queryTokens = tokenize(args.query);
|
|
925
|
+
const corpus = loaded.map((lr) => ({
|
|
926
|
+
id: lr.record_hash,
|
|
927
|
+
tokens: indexableTokensForRecord(lr, annotationsByRecord.get(lr.record_hash)),
|
|
928
|
+
}));
|
|
929
|
+
const idx = buildBM25Index(corpus);
|
|
930
|
+
const now = Date.now();
|
|
931
|
+
const scored = loaded.map((lr) => {
|
|
932
|
+
const r = recencyScore(lr.record.timestamp, now, ATRIB_RECALL_TAU_DAYS);
|
|
933
|
+
const i = importanceScore(annotationsByRecord.get(lr.record_hash));
|
|
934
|
+
const rel = queryTokens.length > 0 ? bm25Score(idx, lr.record_hash, queryTokens) : 0;
|
|
935
|
+
const score = parkScore(r, i, rel, ATRIB_RECALL_ALPHA, ATRIB_RECALL_BETA, ATRIB_RECALL_GAMMA);
|
|
936
|
+
return { lr, score, recency: r, importance: i, relevance: rel };
|
|
937
|
+
});
|
|
938
|
+
scored.sort((a, b) => {
|
|
939
|
+
if (b.score !== a.score)
|
|
940
|
+
return b.score - a.score;
|
|
941
|
+
return b.lr.record.timestamp - a.lr.record.timestamp;
|
|
942
|
+
});
|
|
943
|
+
const k = Math.max(1, Math.min(50, args.k ?? 10));
|
|
944
|
+
const top = scored.slice(0, k);
|
|
945
|
+
return {
|
|
946
|
+
content: [
|
|
947
|
+
{
|
|
948
|
+
type: 'text',
|
|
949
|
+
text: JSON.stringify({
|
|
950
|
+
query: args.query,
|
|
951
|
+
k,
|
|
952
|
+
count: top.length,
|
|
953
|
+
results: top.map(({ lr, score, recency, importance, relevance }) => {
|
|
954
|
+
const ann = annotationsByRecord.get(lr.record_hash);
|
|
955
|
+
return {
|
|
956
|
+
record_hash: lr.record_hash,
|
|
957
|
+
event_type: lr.record.event_type,
|
|
958
|
+
context_id: lr.record.context_id,
|
|
959
|
+
timestamp: lr.record.timestamp,
|
|
960
|
+
tool_name: lr.record.tool_name,
|
|
961
|
+
annotations: ann,
|
|
962
|
+
// Layer 1 v2 legibility fields (parity with compactify).
|
|
963
|
+
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
964
|
+
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
965
|
+
age: formatAge(lr.record.timestamp, now),
|
|
966
|
+
score,
|
|
967
|
+
components: { recency, importance, relevance },
|
|
968
|
+
};
|
|
969
|
+
}),
|
|
970
|
+
}, null, 2),
|
|
971
|
+
},
|
|
972
|
+
],
|
|
973
|
+
};
|
|
974
|
+
}, extractRecordHashFieldsFromMcpResult));
|
|
975
|
+
// recall_session_chain: records in a context_id, ordered chronologically
|
|
976
|
+
// (the natural traversal of the CHAIN_PRECEDES topology). Doable via
|
|
977
|
+
// recall_my_attribution_history({context_id}) + a manual timestamp sort,
|
|
978
|
+
// but agents naturally think "what happened in this session?" and this
|
|
979
|
+
// gives them one call.
|
|
980
|
+
server.registerTool('recall_session_chain', {
|
|
981
|
+
description: "Return all records in a context_id, ordered chronologically (oldest-first). The natural traversal of the CHAIN_PRECEDES topology for a single session/trace. Each entry carries record_hash, event_type, timestamp, display_summary (per-event_type human-readable text from D086), and producer label. Useful for 'what happened in this session?' without manually sorting filter results. Sibling tool to recall_my_attribution_history with built-in context_id scoping + chain-ascending order.",
|
|
982
|
+
inputSchema: {
|
|
983
|
+
context_id: z
|
|
984
|
+
.string()
|
|
985
|
+
.optional()
|
|
986
|
+
.describe("32-hex context_id. When omitted, falls back to @atrib/mcp's resolveEnvContextId (ATRIB_CONTEXT_ID env or D083-registered harness env like CLAUDE_CODE_SESSION_ID)."),
|
|
987
|
+
limit: z
|
|
988
|
+
.number()
|
|
989
|
+
.optional()
|
|
990
|
+
.describe("Maximum records to return (default 50, max 500). Truncated from the END of the chain (oldest-first ordering preserves the chain's start)."),
|
|
991
|
+
include_content: z
|
|
992
|
+
.boolean()
|
|
993
|
+
.optional()
|
|
994
|
+
.describe('When true, include D062 local mirror body as local_content on each returned record. Defaults false to keep the session chain cheap.'),
|
|
995
|
+
},
|
|
996
|
+
}, async (args) => logReadPrimitiveCall('recall_session_chain', args, async () => {
|
|
997
|
+
const ctx = args.context_id ?? resolveEnvContextId();
|
|
998
|
+
if (!ctx) {
|
|
999
|
+
return {
|
|
1000
|
+
content: [
|
|
1001
|
+
{
|
|
1002
|
+
type: 'text',
|
|
1003
|
+
text: JSON.stringify({
|
|
1004
|
+
context_id: null,
|
|
1005
|
+
count: 0,
|
|
1006
|
+
records: [],
|
|
1007
|
+
warning: 'no context_id supplied or resolvable via env',
|
|
1008
|
+
}, null, 2),
|
|
1009
|
+
},
|
|
1010
|
+
],
|
|
1011
|
+
};
|
|
899
1012
|
}
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
},
|
|
909
|
-
],
|
|
910
|
-
};
|
|
911
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
912
|
-
server.registerTool('recall_by_content', {
|
|
913
|
-
description: "Free-form text search over the agent's signed past. Returns top-k records by hybrid retrieval: BM25 over each record's per-event_type indexable text (observation `what + why_noted + topics`; tool_call `tool_name + args + result`; annotation `summary + topics`; revision `prior_position + new_position + reason + topics`; transaction counterparty + memo; directory_anchor tree_root; extension URIs via generic recursive string-walk per D086) plus the annotation summary + topics when present as a curator-quality lift. Reranked by Park et al. weighted-sum scoring with annotation-derived importance and recency signals; BM25 contribution clamped to [0, 1] so the documented Park-component bound is honored. Layer 2 (sqlite-vec sidecar, separate ship) extends with embedding similarity. Useful when the agent has no specific filter and needs to ask 'what do I know about X?'.",
|
|
914
|
-
inputSchema: {
|
|
915
|
-
query: z
|
|
916
|
-
.string()
|
|
917
|
-
.describe("Free-form text query. Matches against each record's per-event_type content plus annotation summary and topics via BM25. Records with no indexable text contribute no relevance signal and only surface through the recency or importance fallback."),
|
|
918
|
-
k: z
|
|
919
|
-
.number()
|
|
920
|
-
.optional()
|
|
921
|
-
.describe('Top-k results to return (default 10, max 50). Final ordering uses Park et al. weighted-sum scoring: alpha*recency + beta*importance + gamma*BM25_relevance. Weights are tunable via ATRIB_RECALL_ALPHA/BETA/GAMMA env vars.'),
|
|
922
|
-
},
|
|
923
|
-
}, async (args) => logReadPrimitiveCall('recall_by_content', args, async () => {
|
|
924
|
-
const { loaded } = discoverLoaded();
|
|
925
|
-
const annotationsByRecord = aggregateAnnotationsByRecord(loaded);
|
|
926
|
-
const queryTokens = tokenize(args.query);
|
|
927
|
-
const corpus = loaded.map((lr) => ({
|
|
928
|
-
id: lr.record_hash,
|
|
929
|
-
tokens: indexableTokensForRecord(lr, annotationsByRecord.get(lr.record_hash)),
|
|
930
|
-
}));
|
|
931
|
-
const idx = buildBM25Index(corpus);
|
|
932
|
-
const now = Date.now();
|
|
933
|
-
const scored = loaded.map((lr) => {
|
|
934
|
-
const r = recencyScore(lr.record.timestamp, now, ATRIB_RECALL_TAU_DAYS);
|
|
935
|
-
const i = importanceScore(annotationsByRecord.get(lr.record_hash));
|
|
936
|
-
const rel = queryTokens.length > 0 ? bm25Score(idx, lr.record_hash, queryTokens) : 0;
|
|
937
|
-
const score = parkScore(r, i, rel, ATRIB_RECALL_ALPHA, ATRIB_RECALL_BETA, ATRIB_RECALL_GAMMA);
|
|
938
|
-
return { lr, score, recency: r, importance: i, relevance: rel };
|
|
939
|
-
});
|
|
940
|
-
scored.sort((a, b) => {
|
|
941
|
-
if (b.score !== a.score)
|
|
942
|
-
return b.score - a.score;
|
|
943
|
-
return b.lr.record.timestamp - a.lr.record.timestamp;
|
|
944
|
-
});
|
|
945
|
-
const k = Math.max(1, Math.min(50, args.k ?? 10));
|
|
946
|
-
const top = scored.slice(0, k);
|
|
947
|
-
return {
|
|
948
|
-
content: [
|
|
949
|
-
{
|
|
950
|
-
type: 'text',
|
|
951
|
-
text: JSON.stringify({
|
|
952
|
-
query: args.query,
|
|
953
|
-
k,
|
|
954
|
-
count: top.length,
|
|
955
|
-
results: top.map(({ lr, score, recency, importance, relevance }) => {
|
|
956
|
-
const ann = annotationsByRecord.get(lr.record_hash);
|
|
957
|
-
return {
|
|
958
|
-
record_hash: lr.record_hash,
|
|
959
|
-
event_type: lr.record.event_type,
|
|
960
|
-
context_id: lr.record.context_id,
|
|
961
|
-
timestamp: lr.record.timestamp,
|
|
962
|
-
tool_name: lr.record.tool_name,
|
|
963
|
-
annotations: ann,
|
|
964
|
-
// Layer 1 v2 legibility fields (parity with compactify).
|
|
965
|
-
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
966
|
-
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
967
|
-
age: formatAge(lr.record.timestamp, now),
|
|
968
|
-
score,
|
|
969
|
-
components: { recency, importance, relevance },
|
|
970
|
-
};
|
|
971
|
-
}),
|
|
972
|
-
}, null, 2),
|
|
973
|
-
},
|
|
974
|
-
],
|
|
975
|
-
};
|
|
976
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
977
|
-
// recall_session_chain: records in a context_id, ordered chronologically
|
|
978
|
-
// (the natural traversal of the CHAIN_PRECEDES topology). Doable via
|
|
979
|
-
// recall_my_attribution_history({context_id}) + a manual timestamp sort,
|
|
980
|
-
// but agents naturally think "what happened in this session?" and this
|
|
981
|
-
// gives them one call.
|
|
982
|
-
server.registerTool('recall_session_chain', {
|
|
983
|
-
description: "Return all records in a context_id, ordered chronologically (oldest-first). The natural traversal of the CHAIN_PRECEDES topology for a single session/trace. Each entry carries record_hash, event_type, timestamp, display_summary (per-event_type human-readable text from D086), and producer label. Useful for 'what happened in this session?' without manually sorting filter results. Sibling tool to recall_my_attribution_history with built-in context_id scoping + chain-ascending order.",
|
|
984
|
-
inputSchema: {
|
|
985
|
-
context_id: z
|
|
986
|
-
.string()
|
|
987
|
-
.optional()
|
|
988
|
-
.describe("32-hex context_id. When omitted, falls back to @atrib/mcp's resolveEnvContextId (ATRIB_CONTEXT_ID env or D083-registered harness env like CLAUDE_CODE_SESSION_ID)."),
|
|
989
|
-
limit: z
|
|
990
|
-
.number()
|
|
991
|
-
.optional()
|
|
992
|
-
.describe("Maximum records to return (default 50, max 500). Truncated from the END of the chain (oldest-first ordering preserves the chain's start)."),
|
|
993
|
-
include_content: z
|
|
994
|
-
.boolean()
|
|
995
|
-
.optional()
|
|
996
|
-
.describe('When true, include D062 local mirror body as local_content on each returned record. Defaults false to keep the session chain cheap.'),
|
|
997
|
-
},
|
|
998
|
-
}, async (args) => logReadPrimitiveCall('recall_session_chain', args, async () => {
|
|
999
|
-
const ctx = args.context_id ?? resolveEnvContextId();
|
|
1000
|
-
if (!ctx) {
|
|
1013
|
+
const { loaded } = discoverLoaded();
|
|
1014
|
+
const annotationsByRecord = aggregateAnnotationsByRecord(loaded);
|
|
1015
|
+
const filtered = loaded
|
|
1016
|
+
.filter((lr) => lr.record.context_id === ctx)
|
|
1017
|
+
.sort((a, b) => a.record.timestamp - b.record.timestamp);
|
|
1018
|
+
const limit = Math.max(1, Math.min(500, args.limit ?? 50));
|
|
1019
|
+
const sliced = filtered.slice(0, limit);
|
|
1020
|
+
const now = Date.now();
|
|
1001
1021
|
return {
|
|
1002
1022
|
content: [
|
|
1003
1023
|
{
|
|
1004
1024
|
type: 'text',
|
|
1005
1025
|
text: JSON.stringify({
|
|
1006
|
-
context_id:
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1026
|
+
context_id: ctx,
|
|
1027
|
+
total: filtered.length,
|
|
1028
|
+
returned: sliced.length,
|
|
1029
|
+
truncated: filtered.length > sliced.length,
|
|
1030
|
+
records: sliced.map((lr) => {
|
|
1031
|
+
const ann = annotationsByRecord.get(lr.record_hash);
|
|
1032
|
+
const entry = {
|
|
1033
|
+
record_hash: lr.record_hash,
|
|
1034
|
+
event_type: lr.record.event_type,
|
|
1035
|
+
timestamp: lr.record.timestamp,
|
|
1036
|
+
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
1037
|
+
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
1038
|
+
age: formatAge(lr.record.timestamp, now),
|
|
1039
|
+
};
|
|
1040
|
+
const informedBy = lr.record
|
|
1041
|
+
.informed_by;
|
|
1042
|
+
const toolName = lr.record.tool_name;
|
|
1043
|
+
const argsHash = lr.record.args_hash;
|
|
1044
|
+
const resultHash = lr.record
|
|
1045
|
+
.result_hash;
|
|
1046
|
+
if (Array.isArray(informedBy) && informedBy.length > 0) {
|
|
1047
|
+
entry.informed_by = informedBy;
|
|
1048
|
+
}
|
|
1049
|
+
if (toolName)
|
|
1050
|
+
entry.tool_name = toolName;
|
|
1051
|
+
if (argsHash)
|
|
1052
|
+
entry.args_hash = argsHash;
|
|
1053
|
+
if (resultHash)
|
|
1054
|
+
entry.result_hash = resultHash;
|
|
1055
|
+
if (args.include_content === true && lr.content !== undefined) {
|
|
1056
|
+
entry.local_content = lr.content;
|
|
1057
|
+
}
|
|
1058
|
+
if (args.include_content === true && lr.producer !== undefined) {
|
|
1059
|
+
entry.local_producer = lr.producer;
|
|
1060
|
+
}
|
|
1061
|
+
return entry;
|
|
1062
|
+
}),
|
|
1010
1063
|
}, null, 2),
|
|
1011
1064
|
},
|
|
1012
1065
|
],
|
|
1013
1066
|
};
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
.result_hash;
|
|
1048
|
-
if (Array.isArray(informedBy) && informedBy.length > 0) {
|
|
1049
|
-
entry.informed_by = informedBy;
|
|
1050
|
-
}
|
|
1051
|
-
if (toolName)
|
|
1052
|
-
entry.tool_name = toolName;
|
|
1053
|
-
if (argsHash)
|
|
1054
|
-
entry.args_hash = argsHash;
|
|
1055
|
-
if (resultHash)
|
|
1056
|
-
entry.result_hash = resultHash;
|
|
1057
|
-
if (args.include_content === true && lr.content !== undefined) {
|
|
1058
|
-
entry.local_content = lr.content;
|
|
1059
|
-
}
|
|
1060
|
-
if (args.include_content === true && lr.producer !== undefined) {
|
|
1061
|
-
entry.local_producer = lr.producer;
|
|
1062
|
-
}
|
|
1063
|
-
return entry;
|
|
1064
|
-
}),
|
|
1065
|
-
}, null, 2),
|
|
1066
|
-
},
|
|
1067
|
-
],
|
|
1068
|
-
};
|
|
1069
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
1070
|
-
// recall_orphans: records nothing else cites via informed_by. Useful for
|
|
1071
|
-
// "what decisions did I make and never act on?" — loose-end discovery.
|
|
1072
|
-
server.registerTool('recall_orphans', {
|
|
1073
|
-
description: "Return records that are NOT cited by any other record via informed_by (loose ends — decisions or observations the agent made but never followed up on). Surfaces records whose record_hash does NOT appear in any other record's informed_by[] array within the local mirror. Optionally scoped to one context_id, one event_type, or one creator_key. Useful for the agent to discover dropped balls (e.g. 'I noted X but never built on it').",
|
|
1074
|
-
inputSchema: {
|
|
1075
|
-
context_id: z
|
|
1076
|
-
.string()
|
|
1077
|
-
.optional()
|
|
1078
|
-
.describe('Optional 32-hex context_id to scope orphan-discovery to one session/trace.'),
|
|
1079
|
-
event_type: EventTypeFilterSchema.optional().describe("Optional filter to one event_type alias or full URI. Most useful with 'observation' to find unfollowed noting/discovery events."),
|
|
1080
|
-
creator_key: z
|
|
1081
|
-
.string()
|
|
1082
|
-
.optional()
|
|
1083
|
-
.describe('Optional exact match on record.creator_key (base64url). Filters orphan-discovery to records signed by one creator.'),
|
|
1084
|
-
limit: z
|
|
1085
|
-
.number()
|
|
1086
|
-
.optional()
|
|
1087
|
-
.describe('Maximum records to return (default 50, max 500), newest-first.'),
|
|
1088
|
-
},
|
|
1089
|
-
}, async (args) => logReadPrimitiveCall('recall_orphans', args, async () => {
|
|
1090
|
-
const { loaded } = discoverLoaded();
|
|
1091
|
-
// Build the set of all record_hashes that appear in any record's
|
|
1092
|
-
// informed_by field. Anything in `loaded` whose record_hash is
|
|
1093
|
-
// NOT in this set is an orphan.
|
|
1094
|
-
const cited = new Set();
|
|
1095
|
-
for (const lr of loaded) {
|
|
1096
|
-
const ib = lr.record.informed_by;
|
|
1097
|
-
if (Array.isArray(ib)) {
|
|
1098
|
-
for (const h of ib)
|
|
1099
|
-
if (typeof h === 'string')
|
|
1100
|
-
cited.add(h);
|
|
1067
|
+
}, extractRecordHashFieldsFromMcpResult));
|
|
1068
|
+
// recall_orphans: records nothing else cites via informed_by. Useful for
|
|
1069
|
+
// "what decisions did I make and never act on?" — loose-end discovery.
|
|
1070
|
+
server.registerTool('recall_orphans', {
|
|
1071
|
+
description: "Return records that are NOT cited by any other record via informed_by (loose ends — decisions or observations the agent made but never followed up on). Surfaces records whose record_hash does NOT appear in any other record's informed_by[] array within the local mirror. Optionally scoped to one context_id, one event_type, or one creator_key. Useful for the agent to discover dropped balls (e.g. 'I noted X but never built on it').",
|
|
1072
|
+
inputSchema: {
|
|
1073
|
+
context_id: z
|
|
1074
|
+
.string()
|
|
1075
|
+
.optional()
|
|
1076
|
+
.describe('Optional 32-hex context_id to scope orphan-discovery to one session/trace.'),
|
|
1077
|
+
event_type: EventTypeFilterSchema.optional().describe("Optional filter to one event_type alias or full URI. Most useful with 'observation' to find unfollowed noting/discovery events."),
|
|
1078
|
+
creator_key: z
|
|
1079
|
+
.string()
|
|
1080
|
+
.optional()
|
|
1081
|
+
.describe('Optional exact match on record.creator_key (base64url). Filters orphan-discovery to records signed by one creator.'),
|
|
1082
|
+
limit: z
|
|
1083
|
+
.number()
|
|
1084
|
+
.optional()
|
|
1085
|
+
.describe('Maximum records to return (default 50, max 500), newest-first.'),
|
|
1086
|
+
},
|
|
1087
|
+
}, async (args) => logReadPrimitiveCall('recall_orphans', args, async () => {
|
|
1088
|
+
const { loaded } = discoverLoaded();
|
|
1089
|
+
// Build the set of all record_hashes that appear in any record's
|
|
1090
|
+
// informed_by field. Anything in `loaded` whose record_hash is
|
|
1091
|
+
// NOT in this set is an orphan.
|
|
1092
|
+
const cited = new Set();
|
|
1093
|
+
for (const lr of loaded) {
|
|
1094
|
+
const ib = lr.record.informed_by;
|
|
1095
|
+
if (Array.isArray(ib)) {
|
|
1096
|
+
for (const h of ib)
|
|
1097
|
+
if (typeof h === 'string')
|
|
1098
|
+
cited.add(h);
|
|
1099
|
+
}
|
|
1101
1100
|
}
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
orphans = orphans.filter((lr) => lr.record.context_id === args.context_id);
|
|
1106
|
-
}
|
|
1107
|
-
if (args.creator_key) {
|
|
1108
|
-
orphans = orphans.filter((lr) => lr.record.creator_key === args.creator_key);
|
|
1109
|
-
}
|
|
1110
|
-
if (args.event_type) {
|
|
1111
|
-
const targetUri = normalizeEventType(args.event_type);
|
|
1112
|
-
orphans = orphans.filter((lr) => lr.record.event_type === targetUri);
|
|
1113
|
-
}
|
|
1114
|
-
orphans.sort((a, b) => b.record.timestamp - a.record.timestamp);
|
|
1115
|
-
const limit = Math.max(1, Math.min(500, args.limit ?? 50));
|
|
1116
|
-
const sliced = orphans.slice(0, limit);
|
|
1117
|
-
const annotationsByRecord = aggregateAnnotationsByRecord(loaded);
|
|
1118
|
-
const now = Date.now();
|
|
1119
|
-
return {
|
|
1120
|
-
content: [
|
|
1121
|
-
{
|
|
1122
|
-
type: 'text',
|
|
1123
|
-
text: JSON.stringify({
|
|
1124
|
-
total: orphans.length,
|
|
1125
|
-
returned: sliced.length,
|
|
1126
|
-
truncated: orphans.length > sliced.length,
|
|
1127
|
-
records: sliced.map((lr) => {
|
|
1128
|
-
const ann = annotationsByRecord.get(lr.record_hash);
|
|
1129
|
-
return {
|
|
1130
|
-
record_hash: lr.record_hash,
|
|
1131
|
-
event_type: lr.record.event_type,
|
|
1132
|
-
context_id: lr.record.context_id,
|
|
1133
|
-
timestamp: lr.record.timestamp,
|
|
1134
|
-
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
1135
|
-
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
1136
|
-
age: formatAge(lr.record.timestamp, now),
|
|
1137
|
-
};
|
|
1138
|
-
}),
|
|
1139
|
-
}, null, 2),
|
|
1140
|
-
},
|
|
1141
|
-
],
|
|
1142
|
-
};
|
|
1143
|
-
}, extractRecordHashFieldsFromMcpResult));
|
|
1144
|
-
// recall_by_signer: aggregate the mirror by creator_key. Lets the agent
|
|
1145
|
-
// ask "who else is in this mirror?" before deciding whether to scope
|
|
1146
|
-
// queries with creator_key filters. Useful when the mirror is shared
|
|
1147
|
-
// across multi-agent flows (transactions with counterparty signers,
|
|
1148
|
-
// peer-shared records, etc.).
|
|
1149
|
-
server.registerTool('recall_by_signer', {
|
|
1150
|
-
description: "Aggregate the local mirror by creator_key. Returns the distinct creators present + per-creator record count + latest record timestamp. Useful when the mirror is multi-signer (multi-agent flows, transactions with counterparty signers) and the agent wants to discover who else's records are in scope before deciding whether to filter with creator_key. Pure aggregation; no records returned directly — use recall_my_attribution_history with the creator_key filter to drill into one creator's records.",
|
|
1151
|
-
inputSchema: {
|
|
1152
|
-
min_records: z
|
|
1153
|
-
.number()
|
|
1154
|
-
.optional()
|
|
1155
|
-
.describe('Optional minimum record count to include a creator in the result. Default 1 (include all).'),
|
|
1156
|
-
},
|
|
1157
|
-
}, async (args) => logReadPrimitiveCall('recall_by_signer', args, async () => {
|
|
1158
|
-
const { loaded } = discoverLoaded();
|
|
1159
|
-
const byKey = new Map();
|
|
1160
|
-
for (const lr of loaded) {
|
|
1161
|
-
const key = lr.record.creator_key;
|
|
1162
|
-
const existing = byKey.get(key);
|
|
1163
|
-
if (existing) {
|
|
1164
|
-
existing.count++;
|
|
1165
|
-
if (lr.record.timestamp > existing.latest_timestamp)
|
|
1166
|
-
existing.latest_timestamp = lr.record.timestamp;
|
|
1167
|
-
if (lr.record.timestamp < existing.earliest_timestamp)
|
|
1168
|
-
existing.earliest_timestamp = lr.record.timestamp;
|
|
1101
|
+
let orphans = loaded.filter((lr) => !cited.has(lr.record_hash));
|
|
1102
|
+
if (args.context_id) {
|
|
1103
|
+
orphans = orphans.filter((lr) => lr.record.context_id === args.context_id);
|
|
1169
1104
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
creator_key: key,
|
|
1173
|
-
count: 1,
|
|
1174
|
-
latest_timestamp: lr.record.timestamp,
|
|
1175
|
-
earliest_timestamp: lr.record.timestamp,
|
|
1176
|
-
});
|
|
1105
|
+
if (args.creator_key) {
|
|
1106
|
+
orphans = orphans.filter((lr) => lr.record.creator_key === args.creator_key);
|
|
1177
1107
|
}
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
.sort((a, b) => b.
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1108
|
+
if (args.event_type) {
|
|
1109
|
+
const targetUri = normalizeEventType(args.event_type);
|
|
1110
|
+
orphans = orphans.filter((lr) => lr.record.event_type === targetUri);
|
|
1111
|
+
}
|
|
1112
|
+
orphans.sort((a, b) => b.record.timestamp - a.record.timestamp);
|
|
1113
|
+
const limit = Math.max(1, Math.min(500, args.limit ?? 50));
|
|
1114
|
+
const sliced = orphans.slice(0, limit);
|
|
1115
|
+
const annotationsByRecord = aggregateAnnotationsByRecord(loaded);
|
|
1116
|
+
const now = Date.now();
|
|
1117
|
+
return {
|
|
1118
|
+
content: [
|
|
1119
|
+
{
|
|
1120
|
+
type: 'text',
|
|
1121
|
+
text: JSON.stringify({
|
|
1122
|
+
total: orphans.length,
|
|
1123
|
+
returned: sliced.length,
|
|
1124
|
+
truncated: orphans.length > sliced.length,
|
|
1125
|
+
records: sliced.map((lr) => {
|
|
1126
|
+
const ann = annotationsByRecord.get(lr.record_hash);
|
|
1127
|
+
return {
|
|
1128
|
+
record_hash: lr.record_hash,
|
|
1129
|
+
event_type: lr.record.event_type,
|
|
1130
|
+
context_id: lr.record.context_id,
|
|
1131
|
+
timestamp: lr.record.timestamp,
|
|
1132
|
+
display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
|
|
1133
|
+
display_producer: resolveDisplayProducer(lr.record, lr.producer),
|
|
1134
|
+
age: formatAge(lr.record.timestamp, now),
|
|
1135
|
+
};
|
|
1136
|
+
}),
|
|
1137
|
+
}, null, 2),
|
|
1138
|
+
},
|
|
1139
|
+
],
|
|
1140
|
+
};
|
|
1141
|
+
}, extractRecordHashFieldsFromMcpResult));
|
|
1142
|
+
// recall_by_signer: aggregate the mirror by creator_key. Lets the agent
|
|
1143
|
+
// ask "who else is in this mirror?" before deciding whether to scope
|
|
1144
|
+
// queries with creator_key filters. Useful when the mirror is shared
|
|
1145
|
+
// across multi-agent flows (transactions with counterparty signers,
|
|
1146
|
+
// peer-shared records, etc.).
|
|
1147
|
+
server.registerTool('recall_by_signer', {
|
|
1148
|
+
description: "Aggregate the local mirror by creator_key. Returns the distinct creators present + per-creator record count + latest record timestamp. Useful when the mirror is multi-signer (multi-agent flows, transactions with counterparty signers) and the agent wants to discover who else's records are in scope before deciding whether to filter with creator_key. Pure aggregation; no records returned directly — use recall_my_attribution_history with the creator_key filter to drill into one creator's records.",
|
|
1149
|
+
inputSchema: {
|
|
1150
|
+
min_records: z
|
|
1151
|
+
.number()
|
|
1152
|
+
.optional()
|
|
1153
|
+
.describe('Optional minimum record count to include a creator in the result. Default 1 (include all).'),
|
|
1154
|
+
},
|
|
1155
|
+
}, async (args) => logReadPrimitiveCall('recall_by_signer', args, async () => {
|
|
1156
|
+
const { loaded } = discoverLoaded();
|
|
1157
|
+
const byKey = new Map();
|
|
1158
|
+
for (const lr of loaded) {
|
|
1159
|
+
const key = lr.record.creator_key;
|
|
1160
|
+
const existing = byKey.get(key);
|
|
1161
|
+
if (existing) {
|
|
1162
|
+
existing.count++;
|
|
1163
|
+
if (lr.record.timestamp > existing.latest_timestamp)
|
|
1164
|
+
existing.latest_timestamp = lr.record.timestamp;
|
|
1165
|
+
if (lr.record.timestamp < existing.earliest_timestamp)
|
|
1166
|
+
existing.earliest_timestamp = lr.record.timestamp;
|
|
1167
|
+
}
|
|
1168
|
+
else {
|
|
1169
|
+
byKey.set(key, {
|
|
1170
|
+
creator_key: key,
|
|
1171
|
+
count: 1,
|
|
1172
|
+
latest_timestamp: lr.record.timestamp,
|
|
1173
|
+
earliest_timestamp: lr.record.timestamp,
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
const minRecords = Math.max(1, args.min_records ?? 1);
|
|
1178
|
+
const stats = [...byKey.values()]
|
|
1179
|
+
.filter((s) => s.count >= minRecords)
|
|
1180
|
+
.sort((a, b) => b.count - a.count);
|
|
1181
|
+
return {
|
|
1182
|
+
content: [
|
|
1183
|
+
{
|
|
1184
|
+
type: 'text',
|
|
1185
|
+
text: JSON.stringify({
|
|
1186
|
+
total_signers: stats.length,
|
|
1187
|
+
total_records: loaded.length,
|
|
1188
|
+
signers: stats,
|
|
1189
|
+
}, null, 2),
|
|
1190
|
+
},
|
|
1191
|
+
],
|
|
1192
|
+
};
|
|
1193
|
+
}, extractRecordHashFieldsFromMcpResult));
|
|
1194
|
+
}
|
|
1195
|
+
export function createAtribRecallServer() {
|
|
1196
|
+
const mcp = new McpServer({
|
|
1197
|
+
name: 'atrib-recall',
|
|
1198
|
+
version: readPackageVersion(),
|
|
1199
|
+
});
|
|
1200
|
+
registerAtribRecallTools(mcp);
|
|
1201
|
+
return { mcp };
|
|
1202
|
+
}
|
|
1203
|
+
async function main() {
|
|
1204
|
+
const { mcp } = createAtribRecallServer();
|
|
1205
|
+
const transport = new StdioServerTransport();
|
|
1206
|
+
await mcp.connect(transport);
|
|
1207
|
+
}
|
|
1208
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
1209
|
+
await main();
|
|
1210
|
+
}
|
|
1198
1211
|
//# sourceMappingURL=index.js.map
|