@atrib/recall 0.14.6 → 1.0.0

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.js CHANGED
@@ -33,13 +33,12 @@
33
33
  */
34
34
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
35
35
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
36
- import { verifyRecord, EVENT_TYPE_SHORT_NAMES, isValidEventTypeUri, normalizeEventType, resolveEnvContextId, logReadPrimitiveCall, } from '@atrib/mcp';
37
- const EventTypeFilterSchema = z.union([
38
- z.enum(EVENT_TYPE_SHORT_NAMES),
39
- z.string().refine((value) => isValidEventTypeUri(value), {
40
- message: 'event_type must be an atrib shorthand alias or a syntactically valid absolute URI',
41
- }),
42
- ]);
36
+ import { verifyRecord, normalizeEventType, resolveEnvContextId, logReadPrimitiveCall, } from '@atrib/mcp';
37
+ // EventTypeFilterSchema moved to ./event-type-filter.js (leaf module) so
38
+ // recall-verb.ts can use it at module init despite the import cycle with
39
+ // this file. Re-exported here for existing importers.
40
+ import { EventTypeFilterSchema } from './event-type-filter.js';
41
+ export { EventTypeFilterSchema };
43
42
  export const IMPORTANCE_NUMERIC = {
44
43
  critical: 5,
45
44
  high: 4,
@@ -128,6 +127,9 @@ import { z } from 'zod';
128
127
  import { aggregateAnnotationsByRecord, aggregateRevisionsByRecord, discoverLoaded, loadLoaded, loadLoadedAppend, loadNewestLoadedFromDir, } from './aggregations.js';
129
128
  import { recencyScore, importanceScore, parkScore, buildBM25Index, bm25Score, bm25ScoresForQuery, normalizedBm25Relevance, operationalToolCallScoreFactor, tokenize, indexableTokensForRecord, shouldSuppressLifecycleAnchorForQuery, queryMentionsLifecycle, } from './scoring.js';
130
129
  import { buildLocalGraph, shortestDistances, walkFrom } from './graph.js';
130
+ import { registerRecallVerbTool } from './recall-verb.js';
131
+ import { extractRecordHashFieldsFromMcpResult, registerTraceTools } from './trace-tools.js';
132
+ import { registerVerifyTool } from './verification.js';
131
133
  import { synthesizeDisplaySummary, resolveDisplayProducer, formatAge } from './legibility.js';
132
134
  const ATRIB_RECORD_FILE = process.env.ATRIB_RECORD_FILE;
133
135
  const ATRIB_MIRROR_DIR = process.env.ATRIB_MIRROR_DIR ?? join(homedir(), '.atrib', 'records');
@@ -603,6 +605,11 @@ function getContentBm25IndexForNewestLimit(snapshot, limit) {
603
605
  snapshot.bm25IndexesByNewestLimit.set(boundedLimit, index);
604
606
  return index;
605
607
  }
608
+ function effectiveToolCallScoreFactor(entry, includeToolCallArgs) {
609
+ if (includeToolCallArgs && entry.tool_call_score_factor < 1)
610
+ return 1;
611
+ return entry.tool_call_score_factor;
612
+ }
606
613
  function hasExplicitRecordLimit(requested) {
607
614
  return typeof requested === 'number' && Number.isFinite(requested);
608
615
  }
@@ -897,43 +904,6 @@ function fullify(bundles) {
897
904
  return out;
898
905
  });
899
906
  }
900
- function extractRecordHashFieldsFromMcpResult(result) {
901
- const seen = new Set();
902
- const pattern = /^sha256:[0-9a-f]{64}$/;
903
- const content = result?.content;
904
- const text = Array.isArray(content) &&
905
- typeof content[0]?.text === 'string'
906
- ? content[0].text
907
- : undefined;
908
- let root = result;
909
- if (text) {
910
- try {
911
- root = JSON.parse(text);
912
- }
913
- catch {
914
- root = result;
915
- }
916
- }
917
- walk(root);
918
- return Array.from(seen);
919
- function walk(node) {
920
- if (node === null || node === undefined)
921
- return;
922
- if (Array.isArray(node)) {
923
- for (const item of node)
924
- walk(item);
925
- return;
926
- }
927
- if (typeof node !== 'object')
928
- return;
929
- const obj = node;
930
- if (typeof obj.record_hash === 'string' && pattern.test(obj.record_hash)) {
931
- seen.add(obj.record_hash);
932
- }
933
- for (const value of Object.values(obj))
934
- walk(value);
935
- }
936
- }
937
907
  /**
938
908
  * Discover and load records per the mirror-discovery contract:
939
909
  * - If `recordFile` is provided, load just that file.
@@ -1128,13 +1098,340 @@ function readPackageVersion() {
1128
1098
  return '0.0.0';
1129
1099
  }
1130
1100
  }
1101
+ export async function runRecallWalk(args) {
1102
+ const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
1103
+ const graph = buildLocalGraph(loaded);
1104
+ const edgeTypes = args.edge_types ? new Set(args.edge_types) : undefined;
1105
+ const depth = typeof args.depth === 'number' ? args.depth : 3;
1106
+ const walk = walkFrom(graph, args.from_record_hash, edgeTypes, depth);
1107
+ // Layer 1 v2 legibility: join walked hashes back to their loaded
1108
+ // records so each step in the walk carries a readable label
1109
+ // instead of just a hash + distance. Builds the index once for
1110
+ // O(N) lookup across the walk; for typical walks (depth=3, k<50)
1111
+ // this is fast.
1112
+ const byHash = new Map(loaded.map((lr) => [lr.record_hash, lr]));
1113
+ const now = Date.now();
1114
+ const enriched = walk.map((step) => {
1115
+ // walk derives from graph (built from `loaded`); byHash always has a hit.
1116
+ const lr = byHash.get(step.record_hash);
1117
+ const ann = annotationsByRecord.get(step.record_hash);
1118
+ return {
1119
+ record_hash: step.record_hash,
1120
+ distance: step.distance,
1121
+ event_type: lr.record.event_type,
1122
+ timestamp: lr.record.timestamp,
1123
+ display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
1124
+ display_producer: resolveDisplayProducer(lr.record, lr.producer),
1125
+ age: formatAge(lr.record.timestamp, now),
1126
+ };
1127
+ });
1128
+ return {
1129
+ from_record_hash: args.from_record_hash,
1130
+ edge_types: args.edge_types ?? ['CHAIN_PRECEDES', 'INFORMED_BY', 'ANNOTATES', 'REVISES'],
1131
+ depth,
1132
+ count: enriched.length,
1133
+ walk: enriched,
1134
+ };
1135
+ }
1136
+ export async function runRecallAnnotations(args) {
1137
+ const { annotationsByRecord } = getLoadedMirrorSnapshot();
1138
+ const summary = annotationsByRecord.get(args.record_hash) ?? null;
1139
+ return { record_hash: args.record_hash, annotations: summary };
1140
+ }
1141
+ export async function runRecallRevisions(args) {
1142
+ const { loaded, revisionsByRecord } = getLoadedMirrorSnapshot();
1143
+ const byHash = new Map();
1144
+ for (const lr of loaded)
1145
+ byHash.set(lr.record_hash, lr);
1146
+ const chain = [];
1147
+ const seen = new Set();
1148
+ let current = args.record_hash;
1149
+ while (!seen.has(current)) {
1150
+ seen.add(current);
1151
+ const next = revisionsByRecord.get(current);
1152
+ if (!next || next.length === 0)
1153
+ break;
1154
+ // Each entry in the map's value array is a revision pointing at
1155
+ // `current`. The chain follows the first-by-timestamp revision;
1156
+ // the remaining entries are surfaced as `sibling_hashes` so the
1157
+ // agent learns that branches exist without the chain shape
1158
+ // having to explode into a tree.
1159
+ const revHash = next[0];
1160
+ const siblings = next.slice(1);
1161
+ const revLr = byHash.get(revHash);
1162
+ const entry = { record_hash: revHash };
1163
+ if (revLr) {
1164
+ entry.timestamp = revLr.record.timestamp;
1165
+ const c = revLr.content;
1166
+ if (c && typeof c === 'object' && !Array.isArray(c)) {
1167
+ const cObj = c;
1168
+ if (typeof cObj.new_position === 'string')
1169
+ entry.new_position = cObj.new_position;
1170
+ if (typeof cObj.reason === 'string')
1171
+ entry.reason = cObj.reason;
1172
+ if (typeof cObj.importance === 'string')
1173
+ entry.importance = cObj.importance;
1174
+ }
1175
+ }
1176
+ if (siblings.length > 0) {
1177
+ entry.sibling_hashes = siblings;
1178
+ }
1179
+ chain.push(entry);
1180
+ current = revHash;
1181
+ }
1182
+ return { record_hash: args.record_hash, revision_chain: chain };
1183
+ }
1184
+ export async function runRecallByContent(args) {
1185
+ const evidenceMode = args.evidence_mode === 'require_complete' ? 'require_complete' : 'bounded';
1186
+ const includeToolCallArgs = args.include_tool_call_args === true;
1187
+ const boundedLimit = resolveBoundedContentSearchLimit(args.max_records, Number.MAX_SAFE_INTEGER);
1188
+ const snapshot = getContentSearchSnapshotForRecall(evidenceMode, boundedLimit);
1189
+ const { entryByHash } = snapshot;
1190
+ const totalRecords = snapshot.totalRecords;
1191
+ const loadedLength = snapshot.entries.length;
1192
+ const searchLimit = evidenceMode === 'require_complete'
1193
+ ? resolveCompleteContentSearchLimit(args.max_records, loadedLength)
1194
+ : Math.min(boundedLimit, loadedLength);
1195
+ const explicitLimit = hasExplicitRecordLimit(args.max_records);
1196
+ if (evidenceMode === 'require_complete' && explicitLimit && searchLimit < loadedLength) {
1197
+ const k = Math.max(1, Math.min(50, args.k ?? 10));
1198
+ return {
1199
+ query: args.query,
1200
+ k,
1201
+ runtime: recallRuntimeMetadata(),
1202
+ ...(includeToolCallArgs ? { include_tool_call_args: true } : {}),
1203
+ evidence_mode: evidenceMode,
1204
+ evidence_status: 'incomplete',
1205
+ fallback_required: true,
1206
+ fallback_reason: `require_complete refused a partial corpus: search_cap=${searchLimit}, ` +
1207
+ `total_records=${loadedLength}.`,
1208
+ fallback: 'Rerun without max_records for full loaded-mirror coverage, or pass an explicit ' +
1209
+ 'partition filter through a caller-owned audit plan and treat each partition as its own coverage claim.',
1210
+ total_records: loadedLength,
1211
+ searched_records: 0,
1212
+ search_cap: searchLimit,
1213
+ coverage: recallCoverage(snapshot, 'incomplete_explicit_limit', 0, snapshot.index),
1214
+ candidate_records: 0,
1215
+ truncated_corpus: true,
1216
+ count: 0,
1217
+ results: [],
1218
+ };
1219
+ }
1220
+ const bm25Index = getContentBm25IndexForNewestLimit(snapshot, searchLimit);
1221
+ const queryTokens = tokenize(args.query);
1222
+ const relevanceByHash = bm25ScoresForQuery(bm25Index, queryTokens);
1223
+ const now = Date.now();
1224
+ const searchPool = queryTokens.length > 0
1225
+ ? Array.from(relevanceByHash.keys())
1226
+ .map((hash) => entryByHash.get(hash))
1227
+ .filter((entry) => entry !== undefined)
1228
+ : snapshot.newestEntries.slice(0, searchLimit);
1229
+ const filteredSearchPool = searchPool.filter((entry) => !(entry.lifecycle_anchor && !queryMentionsLifecycle(queryTokens)));
1230
+ const scored = filteredSearchPool.map((entry) => {
1231
+ const ann = entry.annotations;
1232
+ const suppressLifecycleAnchor = entry.lifecycle_anchor && !queryMentionsLifecycle(queryTokens);
1233
+ const toolCallScoreFactor = effectiveToolCallScoreFactor(entry, includeToolCallArgs);
1234
+ const r = recencyScore(entry.timestamp, now, ATRIB_RECALL_TAU_DAYS) * toolCallScoreFactor;
1235
+ const i = suppressLifecycleAnchor ? 0 : importanceScore(ann);
1236
+ const rawRel = !suppressLifecycleAnchor && queryTokens.length > 0
1237
+ ? (relevanceByHash.get(entry.record_hash) ?? 0)
1238
+ : 0;
1239
+ const rel = !suppressLifecycleAnchor && queryTokens.length > 0
1240
+ ? normalizedBm25Relevance(bm25Index, entry.record_hash, queryTokens, rawRel) *
1241
+ toolCallScoreFactor
1242
+ : 0;
1243
+ const score = parkScore(r, i, rel, ATRIB_RECALL_ALPHA, ATRIB_RECALL_BETA, ATRIB_RECALL_GAMMA);
1244
+ return { entry, score, recency: r, importance: i, relevance: rel };
1245
+ });
1246
+ scored.sort((a, b) => {
1247
+ if (b.score !== a.score)
1248
+ return b.score - a.score;
1249
+ return b.entry.timestamp - a.entry.timestamp;
1250
+ });
1251
+ const k = Math.max(1, Math.min(50, args.k ?? 10));
1252
+ const top = scored.slice(0, k);
1253
+ const completeCoverage = !snapshot.partial && searchLimit >= loadedLength;
1254
+ return {
1255
+ query: args.query,
1256
+ k,
1257
+ runtime: recallRuntimeMetadata(),
1258
+ ...(includeToolCallArgs ? { include_tool_call_args: true } : {}),
1259
+ evidence_mode: evidenceMode,
1260
+ evidence_status: completeCoverage ? 'complete' : 'bounded',
1261
+ fallback_required: false,
1262
+ total_records: totalRecords,
1263
+ searched_records: searchLimit,
1264
+ coverage: recallCoverage(snapshot, completeCoverage ? 'complete_full_scan' : 'bounded_newest_first', searchLimit, snapshot.index),
1265
+ candidate_records: filteredSearchPool.length,
1266
+ truncated_corpus: !completeCoverage,
1267
+ count: top.length,
1268
+ results: top.map(({ entry, score, recency, importance, relevance }) => {
1269
+ return {
1270
+ record_hash: entry.record_hash,
1271
+ event_type: entry.event_type,
1272
+ context_id: entry.context_id,
1273
+ timestamp: entry.timestamp,
1274
+ tool_name: entry.tool_name,
1275
+ annotations: entry.annotations,
1276
+ // Layer 1 v2 legibility fields (parity with compactify).
1277
+ display_summary: entry.display_summary,
1278
+ display_producer: entry.display_producer,
1279
+ age: formatAge(entry.timestamp, now),
1280
+ score,
1281
+ components: { recency, importance, relevance },
1282
+ };
1283
+ }),
1284
+ };
1285
+ }
1286
+ export async function runRecallSessionChain(args) {
1287
+ const ctx = args.context_id ?? resolveEnvContextId();
1288
+ if (!ctx) {
1289
+ return {
1290
+ context_id: null,
1291
+ count: 0,
1292
+ records: [],
1293
+ warning: 'no context_id supplied or resolvable via env',
1294
+ };
1295
+ }
1296
+ const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
1297
+ const filtered = loaded
1298
+ .filter((lr) => lr.record.context_id === ctx)
1299
+ .sort((a, b) => a.record.timestamp - b.record.timestamp);
1300
+ const limit = Math.max(1, Math.min(500, args.limit ?? 50));
1301
+ const sliced = filtered.slice(0, limit);
1302
+ const now = Date.now();
1303
+ return {
1304
+ context_id: ctx,
1305
+ total: filtered.length,
1306
+ returned: sliced.length,
1307
+ truncated: filtered.length > sliced.length,
1308
+ records: sliced.map((lr) => {
1309
+ const ann = annotationsByRecord.get(lr.record_hash);
1310
+ const entry = {
1311
+ record_hash: lr.record_hash,
1312
+ event_type: lr.record.event_type,
1313
+ timestamp: lr.record.timestamp,
1314
+ display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
1315
+ display_producer: resolveDisplayProducer(lr.record, lr.producer),
1316
+ age: formatAge(lr.record.timestamp, now),
1317
+ };
1318
+ const informedBy = lr.record.informed_by;
1319
+ const toolName = lr.record.tool_name;
1320
+ const argsHash = lr.record.args_hash;
1321
+ const resultHash = lr.record.result_hash;
1322
+ if (Array.isArray(informedBy) && informedBy.length > 0) {
1323
+ entry.informed_by = informedBy;
1324
+ }
1325
+ if (toolName)
1326
+ entry.tool_name = toolName;
1327
+ if (argsHash)
1328
+ entry.args_hash = argsHash;
1329
+ if (resultHash)
1330
+ entry.result_hash = resultHash;
1331
+ if (args.include_content === true && lr.content !== undefined) {
1332
+ entry.local_content = lr.content;
1333
+ }
1334
+ if (args.include_content === true && lr.producer !== undefined) {
1335
+ entry.local_producer = lr.producer;
1336
+ }
1337
+ return entry;
1338
+ }),
1339
+ };
1340
+ }
1341
+ export async function runRecallOrphans(args) {
1342
+ const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
1343
+ // Build the set of all record_hashes that appear in any record's
1344
+ // informed_by field. Anything in `loaded` whose record_hash is
1345
+ // NOT in this set is an orphan.
1346
+ const cited = new Set();
1347
+ for (const lr of loaded) {
1348
+ const ib = lr.record.informed_by;
1349
+ if (Array.isArray(ib)) {
1350
+ for (const h of ib)
1351
+ if (typeof h === 'string')
1352
+ cited.add(h);
1353
+ }
1354
+ }
1355
+ let orphans = loaded.filter((lr) => !cited.has(lr.record_hash));
1356
+ if (args.context_id) {
1357
+ orphans = orphans.filter((lr) => lr.record.context_id === args.context_id);
1358
+ }
1359
+ if (args.creator_key) {
1360
+ orphans = orphans.filter((lr) => lr.record.creator_key === args.creator_key);
1361
+ }
1362
+ if (args.event_type) {
1363
+ const targetUri = normalizeEventType(args.event_type);
1364
+ orphans = orphans.filter((lr) => lr.record.event_type === targetUri);
1365
+ }
1366
+ orphans.sort((a, b) => b.record.timestamp - a.record.timestamp);
1367
+ const limit = Math.max(1, Math.min(500, args.limit ?? 50));
1368
+ const sliced = orphans.slice(0, limit);
1369
+ const now = Date.now();
1370
+ return {
1371
+ total: orphans.length,
1372
+ returned: sliced.length,
1373
+ truncated: orphans.length > sliced.length,
1374
+ records: sliced.map((lr) => {
1375
+ const ann = annotationsByRecord.get(lr.record_hash);
1376
+ return {
1377
+ record_hash: lr.record_hash,
1378
+ event_type: lr.record.event_type,
1379
+ context_id: lr.record.context_id,
1380
+ timestamp: lr.record.timestamp,
1381
+ display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
1382
+ display_producer: resolveDisplayProducer(lr.record, lr.producer),
1383
+ age: formatAge(lr.record.timestamp, now),
1384
+ };
1385
+ }),
1386
+ };
1387
+ }
1388
+ export async function runRecallBySigner(args) {
1389
+ const { loaded } = getLoadedMirrorSnapshot();
1390
+ const byKey = new Map();
1391
+ for (const lr of loaded) {
1392
+ const key = lr.record.creator_key;
1393
+ const existing = byKey.get(key);
1394
+ if (existing) {
1395
+ existing.count++;
1396
+ if (lr.record.timestamp > existing.latest_timestamp)
1397
+ existing.latest_timestamp = lr.record.timestamp;
1398
+ if (lr.record.timestamp < existing.earliest_timestamp)
1399
+ existing.earliest_timestamp = lr.record.timestamp;
1400
+ }
1401
+ else {
1402
+ byKey.set(key, {
1403
+ creator_key: key,
1404
+ count: 1,
1405
+ latest_timestamp: lr.record.timestamp,
1406
+ earliest_timestamp: lr.record.timestamp,
1407
+ });
1408
+ }
1409
+ }
1410
+ const minRecords = Math.max(1, args.min_records ?? 1);
1411
+ const stats = [...byKey.values()]
1412
+ .filter((s) => s.count >= minRecords)
1413
+ .sort((a, b) => b.count - a.count);
1414
+ return {
1415
+ total_signers: stats.length,
1416
+ total_records: loaded.length,
1417
+ signers: stats,
1418
+ };
1419
+ }
1420
+ function jsonToolResult(payload) {
1421
+ return {
1422
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
1423
+ };
1424
+ }
1131
1425
  // The recall semantic surface (as defined in the public protocol specification).
1132
- // Eight distinct MCP tools: recall_my_attribution_history is the base
1133
- // filter-and-page tool; recall_annotations + recall_revisions return
1134
- // aggregated annotation summaries / revision chains for a specific
1135
- // record_hash; recall_walk traverses the local Layer 1 derived graph;
1136
- // recall_by_content runs BM25 free-form retrieval; recall_session_chain,
1137
- // recall_orphans, and recall_by_signer cover common agent lookup shapes.
1426
+ // Eight distinct legacy MCP tools kept as permanent aliases over the shape
1427
+ // runners above: recall_my_attribution_history is the base filter-and-page
1428
+ // tool; recall_annotations + recall_revisions return aggregated annotation
1429
+ // summaries / revision chains for a specific record_hash; recall_walk
1430
+ // traverses the local Layer 1 derived graph; recall_by_content runs BM25
1431
+ // free-form retrieval; recall_session_chain, recall_orphans, and
1432
+ // recall_by_signer cover common agent lookup shapes. New callers should
1433
+ // prefer the `recall` verb (one tool, `shape` argument); results are
1434
+ // JSON-identical by construction.
1138
1435
  export function registerAtribRecallTools(server) {
1139
1436
  server.registerTool('recall_my_attribution_history', {
1140
1437
  description: "Return signed atrib records from the local mirror. The agent's own past, with each record's " +
@@ -1149,7 +1446,8 @@ export function registerAtribRecallTools(server) {
1149
1446
  'omit all of them for cross-trace history. Results are sorted newest-first. Pagination uses ' +
1150
1447
  'offset; new records appended between calls invalidate offset stability. See the ' +
1151
1448
  'pagination_caveat in the response. The filtered_out_by_verification field reports how many ' +
1152
- 'records were dropped due to signature failures (always 0 when include_unverified=true).',
1449
+ 'records were dropped due to signature failures (always 0 when include_unverified=true). ' +
1450
+ "Legacy alias: new callers should prefer the `recall` tool with shape='history'.",
1153
1451
  inputSchema: {
1154
1452
  context_id: z
1155
1453
  .string()
@@ -1264,20 +1562,13 @@ export function registerAtribRecallTools(server) {
1264
1562
  // min_importance, topic_tags, include_revised, min_signers,
1265
1563
  // rank_by, rank_anchor, and toc.
1266
1564
  const result = await recall(args);
1267
- return {
1268
- content: [
1269
- {
1270
- type: 'text',
1271
- text: JSON.stringify(result, null, 2),
1272
- },
1273
- ],
1274
- };
1565
+ return jsonToolResult(result);
1275
1566
  }, extractRecordHashFieldsFromMcpResult));
1276
1567
  // ─── Layer 1 sibling tools ───
1277
1568
  // Sibling tools expose common agent lookup shapes beyond the base
1278
1569
  // filter-and-page tool.
1279
1570
  server.registerTool('recall_walk', {
1280
- 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.',
1571
+ 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. Legacy alias: new callers should prefer the `recall` tool with shape=\'walk\' (no direction).',
1281
1572
  inputSchema: {
1282
1573
  from_record_hash: z
1283
1574
  .string()
@@ -1291,131 +1582,29 @@ export function registerAtribRecallTools(server) {
1291
1582
  .optional()
1292
1583
  .describe('Maximum hop count (NOT cumulative weight). Default 3. Higher values may return many records; paginate downstream if needed.'),
1293
1584
  },
1294
- }, async (args) => logReadPrimitiveCall('recall_walk', args, async () => {
1295
- const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
1296
- const graph = buildLocalGraph(loaded);
1297
- const edgeTypes = args.edge_types ? new Set(args.edge_types) : undefined;
1298
- const depth = typeof args.depth === 'number' ? args.depth : 3;
1299
- const walk = walkFrom(graph, args.from_record_hash, edgeTypes, depth);
1300
- // Layer 1 v2 legibility: join walked hashes back to their loaded
1301
- // records so each step in the walk carries a readable label
1302
- // instead of just a hash + distance. Builds the index once for
1303
- // O(N) lookup across the walk; for typical walks (depth=3, k<50)
1304
- // this is fast.
1305
- const byHash = new Map(loaded.map((lr) => [lr.record_hash, lr]));
1306
- const now = Date.now();
1307
- const enriched = walk.map((step) => {
1308
- // walk derives from graph (built from `loaded`); byHash always has a hit.
1309
- const lr = byHash.get(step.record_hash);
1310
- const ann = annotationsByRecord.get(step.record_hash);
1311
- return {
1312
- record_hash: step.record_hash,
1313
- distance: step.distance,
1314
- event_type: lr.record.event_type,
1315
- timestamp: lr.record.timestamp,
1316
- display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
1317
- display_producer: resolveDisplayProducer(lr.record, lr.producer),
1318
- age: formatAge(lr.record.timestamp, now),
1319
- };
1320
- });
1321
- return {
1322
- content: [
1323
- {
1324
- type: 'text',
1325
- text: JSON.stringify({
1326
- from_record_hash: args.from_record_hash,
1327
- edge_types: args.edge_types ?? [
1328
- 'CHAIN_PRECEDES',
1329
- 'INFORMED_BY',
1330
- 'ANNOTATES',
1331
- 'REVISES',
1332
- ],
1333
- depth,
1334
- count: enriched.length,
1335
- walk: enriched,
1336
- }, null, 2),
1337
- },
1338
- ],
1339
- };
1340
- }, extractRecordHashFieldsFromMcpResult));
1585
+ }, async (args) => logReadPrimitiveCall('recall_walk', args, async () => jsonToolResult(await runRecallWalk({
1586
+ from_record_hash: args.from_record_hash,
1587
+ edge_types: args.edge_types,
1588
+ depth: typeof args.depth === 'number' ? args.depth : undefined,
1589
+ })), extractRecordHashFieldsFromMcpResult));
1341
1590
  server.registerTool('recall_annotations', {
1342
- 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.",
1591
+ 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. Legacy alias: new callers should prefer the `recall` tool with shape='annotations'.",
1343
1592
  inputSchema: {
1344
1593
  record_hash: z
1345
1594
  .string()
1346
1595
  .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.'),
1347
1596
  },
1348
- }, async (args) => logReadPrimitiveCall('recall_annotations', args, async () => {
1349
- const { annotationsByRecord } = getLoadedMirrorSnapshot();
1350
- const summary = annotationsByRecord.get(args.record_hash) ?? null;
1351
- return {
1352
- content: [
1353
- {
1354
- type: 'text',
1355
- text: JSON.stringify({ record_hash: args.record_hash, annotations: summary }, null, 2),
1356
- },
1357
- ],
1358
- };
1359
- }, extractRecordHashFieldsFromMcpResult));
1597
+ }, async (args) => logReadPrimitiveCall('recall_annotations', args, async () => jsonToolResult(await runRecallAnnotations(args)), extractRecordHashFieldsFromMcpResult));
1360
1598
  server.registerTool('recall_revisions', {
1361
- 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.",
1599
+ 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. Legacy alias: new callers should prefer the `recall` tool with shape='revisions'.",
1362
1600
  inputSchema: {
1363
1601
  record_hash: z
1364
1602
  .string()
1365
1603
  .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).'),
1366
1604
  },
1367
- }, async (args) => logReadPrimitiveCall('recall_revisions', args, async () => {
1368
- const { loaded, revisionsByRecord } = getLoadedMirrorSnapshot();
1369
- const byHash = new Map();
1370
- for (const lr of loaded)
1371
- byHash.set(lr.record_hash, lr);
1372
- const chain = [];
1373
- const seen = new Set();
1374
- let current = args.record_hash;
1375
- while (!seen.has(current)) {
1376
- seen.add(current);
1377
- const next = revisionsByRecord.get(current);
1378
- if (!next || next.length === 0)
1379
- break;
1380
- // Each entry in the map's value array is a revision pointing at
1381
- // `current`. The chain follows the first-by-timestamp revision;
1382
- // the remaining entries are surfaced as `sibling_hashes` so the
1383
- // agent learns that branches exist without the chain shape
1384
- // having to explode into a tree.
1385
- const revHash = next[0];
1386
- const siblings = next.slice(1);
1387
- const revLr = byHash.get(revHash);
1388
- const entry = { record_hash: revHash };
1389
- if (revLr) {
1390
- entry.timestamp = revLr.record.timestamp;
1391
- const c = revLr.content;
1392
- if (c && typeof c === 'object' && !Array.isArray(c)) {
1393
- const cObj = c;
1394
- if (typeof cObj.new_position === 'string')
1395
- entry.new_position = cObj.new_position;
1396
- if (typeof cObj.reason === 'string')
1397
- entry.reason = cObj.reason;
1398
- if (typeof cObj.importance === 'string')
1399
- entry.importance = cObj.importance;
1400
- }
1401
- }
1402
- if (siblings.length > 0) {
1403
- entry.sibling_hashes = siblings;
1404
- }
1405
- chain.push(entry);
1406
- current = revHash;
1407
- }
1408
- return {
1409
- content: [
1410
- {
1411
- type: 'text',
1412
- text: JSON.stringify({ record_hash: args.record_hash, revision_chain: chain }, null, 2),
1413
- },
1414
- ],
1415
- };
1416
- }, extractRecordHashFieldsFromMcpResult));
1605
+ }, async (args) => logReadPrimitiveCall('recall_revisions', args, async () => jsonToolResult(await runRecallRevisions(args)), extractRecordHashFieldsFromMcpResult));
1417
1606
  server.registerTool('recall_by_content', {
1418
- 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 + intent + rationale + 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/D118) 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. Raw unannotated tool_call records are score-demoted so operation logs do not dominate substantive memory. BM25 contribution is clamped to [0, 1] before coverage scaling so the documented Park-component bound is honored. Responses include runtime metadata plus coverage.index, so callers can detect stale MCP processes and whether the durable content-token sidecar was hit, rebuilt, disabled, or bypassed. Useful when the agent has no specific filter and needs to ask 'what do I know about X?'.",
1607
+ 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 + intent + rationale + 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/D118) 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. Raw unannotated tool_call records are score-demoted so operation logs do not dominate substantive memory. BM25 contribution is clamped to [0, 1] before coverage scaling so the documented Park-component bound is honored. Responses include runtime metadata plus coverage.index, so callers can detect stale MCP processes and whether the durable content-token sidecar was hit, rebuilt, disabled, or bypassed. Useful when the agent has no specific filter and needs to ask 'what do I know about X?'. Legacy alias: new callers should prefer the `recall` tool with shape='content'.",
1419
1608
  inputSchema: {
1420
1609
  query: z
1421
1610
  .string()
@@ -1435,127 +1624,19 @@ export function registerAtribRecallTools(server) {
1435
1624
  'Use require_complete for critical-path audits: it loads the full mirror, searches every ' +
1436
1625
  'loaded record, and refuses partial results with evidence_status=incomplete plus ' +
1437
1626
  'fallback_required=true when max_records is explicitly below total_records.'),
1627
+ include_tool_call_args: z
1628
+ .boolean()
1629
+ .optional()
1630
+ .describe('Lift the operational tool_call score suppression for this query so unannotated tool_call records, including their indexed args and result excerpts, rank by ordinary recency and relevance. The default keeps them down-weighted so operational noise does not dominate conversational recall.'),
1438
1631
  },
1439
- }, async (args) => logReadPrimitiveCall('recall_by_content', args, async () => {
1440
- const evidenceMode = args.evidence_mode === 'require_complete' ? 'require_complete' : 'bounded';
1441
- const boundedLimit = resolveBoundedContentSearchLimit(args.max_records, Number.MAX_SAFE_INTEGER);
1442
- const snapshot = getContentSearchSnapshotForRecall(evidenceMode, boundedLimit);
1443
- const { entryByHash } = snapshot;
1444
- const totalRecords = snapshot.totalRecords;
1445
- const loadedLength = snapshot.entries.length;
1446
- const searchLimit = evidenceMode === 'require_complete'
1447
- ? resolveCompleteContentSearchLimit(args.max_records, loadedLength)
1448
- : Math.min(boundedLimit, loadedLength);
1449
- const explicitLimit = hasExplicitRecordLimit(args.max_records);
1450
- if (evidenceMode === 'require_complete' && explicitLimit && searchLimit < loadedLength) {
1451
- const k = Math.max(1, Math.min(50, args.k ?? 10));
1452
- return {
1453
- content: [
1454
- {
1455
- type: 'text',
1456
- text: JSON.stringify({
1457
- query: args.query,
1458
- k,
1459
- runtime: recallRuntimeMetadata(),
1460
- evidence_mode: evidenceMode,
1461
- evidence_status: 'incomplete',
1462
- fallback_required: true,
1463
- fallback_reason: `require_complete refused a partial corpus: search_cap=${searchLimit}, ` +
1464
- `total_records=${loadedLength}.`,
1465
- fallback: 'Rerun without max_records for full loaded-mirror coverage, or pass an explicit ' +
1466
- 'partition filter through a caller-owned audit plan and treat each partition as its own coverage claim.',
1467
- total_records: loadedLength,
1468
- searched_records: 0,
1469
- search_cap: searchLimit,
1470
- coverage: recallCoverage(snapshot, 'incomplete_explicit_limit', 0, snapshot.index),
1471
- candidate_records: 0,
1472
- truncated_corpus: true,
1473
- count: 0,
1474
- results: [],
1475
- }, null, 2),
1476
- },
1477
- ],
1478
- };
1479
- }
1480
- const bm25Index = getContentBm25IndexForNewestLimit(snapshot, searchLimit);
1481
- const queryTokens = tokenize(args.query);
1482
- const relevanceByHash = bm25ScoresForQuery(bm25Index, queryTokens);
1483
- const now = Date.now();
1484
- const searchPool = queryTokens.length > 0
1485
- ? Array.from(relevanceByHash.keys())
1486
- .map((hash) => entryByHash.get(hash))
1487
- .filter((entry) => entry !== undefined)
1488
- : snapshot.newestEntries.slice(0, searchLimit);
1489
- const filteredSearchPool = searchPool.filter((entry) => !(entry.lifecycle_anchor && !queryMentionsLifecycle(queryTokens)));
1490
- const scored = filteredSearchPool.map((entry) => {
1491
- const ann = entry.annotations;
1492
- const suppressLifecycleAnchor = entry.lifecycle_anchor && !queryMentionsLifecycle(queryTokens);
1493
- const toolCallScoreFactor = entry.tool_call_score_factor;
1494
- const r = recencyScore(entry.timestamp, now, ATRIB_RECALL_TAU_DAYS) * toolCallScoreFactor;
1495
- const i = suppressLifecycleAnchor ? 0 : importanceScore(ann);
1496
- const rawRel = !suppressLifecycleAnchor && queryTokens.length > 0
1497
- ? (relevanceByHash.get(entry.record_hash) ?? 0)
1498
- : 0;
1499
- const rel = !suppressLifecycleAnchor && queryTokens.length > 0
1500
- ? normalizedBm25Relevance(bm25Index, entry.record_hash, queryTokens, rawRel) *
1501
- toolCallScoreFactor
1502
- : 0;
1503
- const score = parkScore(r, i, rel, ATRIB_RECALL_ALPHA, ATRIB_RECALL_BETA, ATRIB_RECALL_GAMMA);
1504
- return { entry, score, recency: r, importance: i, relevance: rel };
1505
- });
1506
- scored.sort((a, b) => {
1507
- if (b.score !== a.score)
1508
- return b.score - a.score;
1509
- return b.entry.timestamp - a.entry.timestamp;
1510
- });
1511
- const k = Math.max(1, Math.min(50, args.k ?? 10));
1512
- const top = scored.slice(0, k);
1513
- const completeCoverage = !snapshot.partial && searchLimit >= loadedLength;
1514
- return {
1515
- content: [
1516
- {
1517
- type: 'text',
1518
- text: JSON.stringify({
1519
- query: args.query,
1520
- k,
1521
- runtime: recallRuntimeMetadata(),
1522
- evidence_mode: evidenceMode,
1523
- evidence_status: completeCoverage ? 'complete' : 'bounded',
1524
- fallback_required: false,
1525
- total_records: totalRecords,
1526
- searched_records: searchLimit,
1527
- coverage: recallCoverage(snapshot, completeCoverage ? 'complete_full_scan' : 'bounded_newest_first', searchLimit, snapshot.index),
1528
- candidate_records: filteredSearchPool.length,
1529
- truncated_corpus: !completeCoverage,
1530
- count: top.length,
1531
- results: top.map(({ entry, score, recency, importance, relevance }) => {
1532
- return {
1533
- record_hash: entry.record_hash,
1534
- event_type: entry.event_type,
1535
- context_id: entry.context_id,
1536
- timestamp: entry.timestamp,
1537
- tool_name: entry.tool_name,
1538
- annotations: entry.annotations,
1539
- // Layer 1 v2 legibility fields (parity with compactify).
1540
- display_summary: entry.display_summary,
1541
- display_producer: entry.display_producer,
1542
- age: formatAge(entry.timestamp, now),
1543
- score,
1544
- components: { recency, importance, relevance },
1545
- };
1546
- }),
1547
- }, null, 2),
1548
- },
1549
- ],
1550
- };
1551
- }, extractRecordHashFieldsFromMcpResult));
1632
+ }, async (args) => logReadPrimitiveCall('recall_by_content', args, async () => jsonToolResult(await runRecallByContent(args)), extractRecordHashFieldsFromMcpResult));
1552
1633
  // recall_session_chain: records in a context_id, ordered chronologically
1553
1634
  // (the natural traversal of the CHAIN_PRECEDES topology). Doable via
1554
1635
  // recall_my_attribution_history({context_id}) + a manual timestamp sort,
1555
1636
  // but agents naturally think "what happened in this session?" and this
1556
1637
  // gives them one call.
1557
1638
  server.registerTool('recall_session_chain', {
1558
- 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.",
1639
+ 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. Legacy alias: new callers should prefer the `recall` tool with shape='chain'.",
1559
1640
  inputSchema: {
1560
1641
  context_id: z
1561
1642
  .string()
@@ -1570,81 +1651,11 @@ export function registerAtribRecallTools(server) {
1570
1651
  .optional()
1571
1652
  .describe('When true, include D062 local mirror body as local_content on each returned record. Defaults false to keep the session chain cheap.'),
1572
1653
  },
1573
- }, async (args) => logReadPrimitiveCall('recall_session_chain', args, async () => {
1574
- const ctx = args.context_id ?? resolveEnvContextId();
1575
- if (!ctx) {
1576
- return {
1577
- content: [
1578
- {
1579
- type: 'text',
1580
- text: JSON.stringify({
1581
- context_id: null,
1582
- count: 0,
1583
- records: [],
1584
- warning: 'no context_id supplied or resolvable via env',
1585
- }, null, 2),
1586
- },
1587
- ],
1588
- };
1589
- }
1590
- const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
1591
- const filtered = loaded
1592
- .filter((lr) => lr.record.context_id === ctx)
1593
- .sort((a, b) => a.record.timestamp - b.record.timestamp);
1594
- const limit = Math.max(1, Math.min(500, args.limit ?? 50));
1595
- const sliced = filtered.slice(0, limit);
1596
- const now = Date.now();
1597
- return {
1598
- content: [
1599
- {
1600
- type: 'text',
1601
- text: JSON.stringify({
1602
- context_id: ctx,
1603
- total: filtered.length,
1604
- returned: sliced.length,
1605
- truncated: filtered.length > sliced.length,
1606
- records: sliced.map((lr) => {
1607
- const ann = annotationsByRecord.get(lr.record_hash);
1608
- const entry = {
1609
- record_hash: lr.record_hash,
1610
- event_type: lr.record.event_type,
1611
- timestamp: lr.record.timestamp,
1612
- display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
1613
- display_producer: resolveDisplayProducer(lr.record, lr.producer),
1614
- age: formatAge(lr.record.timestamp, now),
1615
- };
1616
- const informedBy = lr.record
1617
- .informed_by;
1618
- const toolName = lr.record.tool_name;
1619
- const argsHash = lr.record.args_hash;
1620
- const resultHash = lr.record
1621
- .result_hash;
1622
- if (Array.isArray(informedBy) && informedBy.length > 0) {
1623
- entry.informed_by = informedBy;
1624
- }
1625
- if (toolName)
1626
- entry.tool_name = toolName;
1627
- if (argsHash)
1628
- entry.args_hash = argsHash;
1629
- if (resultHash)
1630
- entry.result_hash = resultHash;
1631
- if (args.include_content === true && lr.content !== undefined) {
1632
- entry.local_content = lr.content;
1633
- }
1634
- if (args.include_content === true && lr.producer !== undefined) {
1635
- entry.local_producer = lr.producer;
1636
- }
1637
- return entry;
1638
- }),
1639
- }, null, 2),
1640
- },
1641
- ],
1642
- };
1643
- }, extractRecordHashFieldsFromMcpResult));
1654
+ }, async (args) => logReadPrimitiveCall('recall_session_chain', args, async () => jsonToolResult(await runRecallSessionChain(args)), extractRecordHashFieldsFromMcpResult));
1644
1655
  // recall_orphans: records nothing else cites via informed_by. Useful for
1645
1656
  // "what decisions did I make and never act on?": loose-end discovery.
1646
1657
  server.registerTool('recall_orphans', {
1647
- 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').",
1658
+ 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'). Legacy alias: new callers should prefer the `recall` tool with shape='orphans'.",
1648
1659
  inputSchema: {
1649
1660
  context_id: z
1650
1661
  .string()
@@ -1660,119 +1671,47 @@ export function registerAtribRecallTools(server) {
1660
1671
  .optional()
1661
1672
  .describe('Maximum records to return (default 50, max 500), newest-first.'),
1662
1673
  },
1663
- }, async (args) => logReadPrimitiveCall('recall_orphans', args, async () => {
1664
- const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
1665
- // Build the set of all record_hashes that appear in any record's
1666
- // informed_by field. Anything in `loaded` whose record_hash is
1667
- // NOT in this set is an orphan.
1668
- const cited = new Set();
1669
- for (const lr of loaded) {
1670
- const ib = lr.record.informed_by;
1671
- if (Array.isArray(ib)) {
1672
- for (const h of ib)
1673
- if (typeof h === 'string')
1674
- cited.add(h);
1675
- }
1676
- }
1677
- let orphans = loaded.filter((lr) => !cited.has(lr.record_hash));
1678
- if (args.context_id) {
1679
- orphans = orphans.filter((lr) => lr.record.context_id === args.context_id);
1680
- }
1681
- if (args.creator_key) {
1682
- orphans = orphans.filter((lr) => lr.record.creator_key === args.creator_key);
1683
- }
1684
- if (args.event_type) {
1685
- const targetUri = normalizeEventType(args.event_type);
1686
- orphans = orphans.filter((lr) => lr.record.event_type === targetUri);
1687
- }
1688
- orphans.sort((a, b) => b.record.timestamp - a.record.timestamp);
1689
- const limit = Math.max(1, Math.min(500, args.limit ?? 50));
1690
- const sliced = orphans.slice(0, limit);
1691
- const now = Date.now();
1692
- return {
1693
- content: [
1694
- {
1695
- type: 'text',
1696
- text: JSON.stringify({
1697
- total: orphans.length,
1698
- returned: sliced.length,
1699
- truncated: orphans.length > sliced.length,
1700
- records: sliced.map((lr) => {
1701
- const ann = annotationsByRecord.get(lr.record_hash);
1702
- return {
1703
- record_hash: lr.record_hash,
1704
- event_type: lr.record.event_type,
1705
- context_id: lr.record.context_id,
1706
- timestamp: lr.record.timestamp,
1707
- display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
1708
- display_producer: resolveDisplayProducer(lr.record, lr.producer),
1709
- age: formatAge(lr.record.timestamp, now),
1710
- };
1711
- }),
1712
- }, null, 2),
1713
- },
1714
- ],
1715
- };
1716
- }, extractRecordHashFieldsFromMcpResult));
1674
+ }, async (args) => logReadPrimitiveCall('recall_orphans', args, async () => jsonToolResult(await runRecallOrphans(args)), extractRecordHashFieldsFromMcpResult));
1717
1675
  // recall_by_signer: aggregate the mirror by creator_key. Lets the agent
1718
1676
  // ask "who else is in this mirror?" before deciding whether to scope
1719
1677
  // queries with creator_key filters. Useful when the mirror is shared
1720
1678
  // across multi-agent flows (transactions with counterparty signers,
1721
1679
  // peer-shared records, etc.).
1722
1680
  server.registerTool('recall_by_signer', {
1723
- 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.",
1681
+ 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. Legacy alias: new callers should prefer the `recall` tool with shape='by_signer'.",
1724
1682
  inputSchema: {
1725
1683
  min_records: z
1726
1684
  .number()
1727
1685
  .optional()
1728
1686
  .describe('Optional minimum record count to include a creator in the result. Default 1 (include all).'),
1729
1687
  },
1730
- }, async (args) => logReadPrimitiveCall('recall_by_signer', args, async () => {
1731
- const { loaded } = getLoadedMirrorSnapshot();
1732
- const byKey = new Map();
1733
- for (const lr of loaded) {
1734
- const key = lr.record.creator_key;
1735
- const existing = byKey.get(key);
1736
- if (existing) {
1737
- existing.count++;
1738
- if (lr.record.timestamp > existing.latest_timestamp)
1739
- existing.latest_timestamp = lr.record.timestamp;
1740
- if (lr.record.timestamp < existing.earliest_timestamp)
1741
- existing.earliest_timestamp = lr.record.timestamp;
1742
- }
1743
- else {
1744
- byKey.set(key, {
1745
- creator_key: key,
1746
- count: 1,
1747
- latest_timestamp: lr.record.timestamp,
1748
- earliest_timestamp: lr.record.timestamp,
1749
- });
1750
- }
1751
- }
1752
- const minRecords = Math.max(1, args.min_records ?? 1);
1753
- const stats = [...byKey.values()]
1754
- .filter((s) => s.count >= minRecords)
1755
- .sort((a, b) => b.count - a.count);
1756
- return {
1757
- content: [
1758
- {
1759
- type: 'text',
1760
- text: JSON.stringify({
1761
- total_signers: stats.length,
1762
- total_records: loaded.length,
1763
- signers: stats,
1764
- }, null, 2),
1765
- },
1766
- ],
1767
- };
1768
- }, extractRecordHashFieldsFromMcpResult));
1688
+ }, async (args) => logReadPrimitiveCall('recall_by_signer', args, async () => jsonToolResult(await runRecallBySigner(args)), extractRecordHashFieldsFromMcpResult));
1689
+ }
1690
+ // ─── Re-export surface for the @atrib/trace and @atrib/verify-mcp shims
1691
+ // and for embedders. The implementations live in this package per the
1692
+ // attest/recall rename; the legacy packages re-export them.
1693
+ export { compactVisited, createAtribTraceServer, registerTraceTools, runTraceWalk, summarizeSidecar, TraceInput, } from './trace-tools.js';
1694
+ export { extractRecordHashFieldsFromMcpResult };
1695
+ export { createAtribVerifyServer, handleAtribVerify, loadVerifyModule, registerVerifyTool, tryHandleAtribVerify, VerifyInput, } from './verification.js';
1696
+ export { registerRecallVerbTool, RecallVerbInput, RECALL_SHAPES, runRecallVerb } from './recall-verb.js';
1697
+ /**
1698
+ * Register the full read union on one server: the `recall` verb plus every
1699
+ * legacy read tool name it absorbs (8 recall_* tools, trace, trace_forward,
1700
+ * atrib-verify). This is what the `atrib-recall` binary serves and what the
1701
+ * primitive runtime / daemon mount for reads (alias-window rule W1).
1702
+ */
1703
+ export function registerAtribReadTools(server) {
1704
+ registerRecallVerbTool(server);
1705
+ registerAtribRecallTools(server);
1706
+ registerTraceTools(server);
1707
+ registerVerifyTool(server);
1769
1708
  }
1770
1709
  export function createAtribRecallServer() {
1771
1710
  const mcp = new McpServer({
1772
1711
  name: 'atrib-recall',
1773
1712
  version: readPackageVersion(),
1774
1713
  });
1775
- registerAtribRecallTools(mcp);
1714
+ registerAtribReadTools(mcp);
1776
1715
  return { mcp };
1777
1716
  }
1778
1717
  async function main() {