@atrib/recall 0.14.7 → 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');
@@ -902,43 +904,6 @@ function fullify(bundles) {
902
904
  return out;
903
905
  });
904
906
  }
905
- function extractRecordHashFieldsFromMcpResult(result) {
906
- const seen = new Set();
907
- const pattern = /^sha256:[0-9a-f]{64}$/;
908
- const content = result?.content;
909
- const text = Array.isArray(content) &&
910
- typeof content[0]?.text === 'string'
911
- ? content[0].text
912
- : undefined;
913
- let root = result;
914
- if (text) {
915
- try {
916
- root = JSON.parse(text);
917
- }
918
- catch {
919
- root = result;
920
- }
921
- }
922
- walk(root);
923
- return Array.from(seen);
924
- function walk(node) {
925
- if (node === null || node === undefined)
926
- return;
927
- if (Array.isArray(node)) {
928
- for (const item of node)
929
- walk(item);
930
- return;
931
- }
932
- if (typeof node !== 'object')
933
- return;
934
- const obj = node;
935
- if (typeof obj.record_hash === 'string' && pattern.test(obj.record_hash)) {
936
- seen.add(obj.record_hash);
937
- }
938
- for (const value of Object.values(obj))
939
- walk(value);
940
- }
941
- }
942
907
  /**
943
908
  * Discover and load records per the mirror-discovery contract:
944
909
  * - If `recordFile` is provided, load just that file.
@@ -1133,13 +1098,340 @@ function readPackageVersion() {
1133
1098
  return '0.0.0';
1134
1099
  }
1135
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
+ }
1136
1425
  // The recall semantic surface (as defined in the public protocol specification).
1137
- // Eight distinct MCP tools: recall_my_attribution_history is the base
1138
- // filter-and-page tool; recall_annotations + recall_revisions return
1139
- // aggregated annotation summaries / revision chains for a specific
1140
- // record_hash; recall_walk traverses the local Layer 1 derived graph;
1141
- // recall_by_content runs BM25 free-form retrieval; recall_session_chain,
1142
- // 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.
1143
1435
  export function registerAtribRecallTools(server) {
1144
1436
  server.registerTool('recall_my_attribution_history', {
1145
1437
  description: "Return signed atrib records from the local mirror. The agent's own past, with each record's " +
@@ -1154,7 +1446,8 @@ export function registerAtribRecallTools(server) {
1154
1446
  'omit all of them for cross-trace history. Results are sorted newest-first. Pagination uses ' +
1155
1447
  'offset; new records appended between calls invalidate offset stability. See the ' +
1156
1448
  'pagination_caveat in the response. The filtered_out_by_verification field reports how many ' +
1157
- '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'.",
1158
1451
  inputSchema: {
1159
1452
  context_id: z
1160
1453
  .string()
@@ -1269,20 +1562,13 @@ export function registerAtribRecallTools(server) {
1269
1562
  // min_importance, topic_tags, include_revised, min_signers,
1270
1563
  // rank_by, rank_anchor, and toc.
1271
1564
  const result = await recall(args);
1272
- return {
1273
- content: [
1274
- {
1275
- type: 'text',
1276
- text: JSON.stringify(result, null, 2),
1277
- },
1278
- ],
1279
- };
1565
+ return jsonToolResult(result);
1280
1566
  }, extractRecordHashFieldsFromMcpResult));
1281
1567
  // ─── Layer 1 sibling tools ───
1282
1568
  // Sibling tools expose common agent lookup shapes beyond the base
1283
1569
  // filter-and-page tool.
1284
1570
  server.registerTool('recall_walk', {
1285
- 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).',
1286
1572
  inputSchema: {
1287
1573
  from_record_hash: z
1288
1574
  .string()
@@ -1296,131 +1582,29 @@ export function registerAtribRecallTools(server) {
1296
1582
  .optional()
1297
1583
  .describe('Maximum hop count (NOT cumulative weight). Default 3. Higher values may return many records; paginate downstream if needed.'),
1298
1584
  },
1299
- }, async (args) => logReadPrimitiveCall('recall_walk', args, async () => {
1300
- const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
1301
- const graph = buildLocalGraph(loaded);
1302
- const edgeTypes = args.edge_types ? new Set(args.edge_types) : undefined;
1303
- const depth = typeof args.depth === 'number' ? args.depth : 3;
1304
- const walk = walkFrom(graph, args.from_record_hash, edgeTypes, depth);
1305
- // Layer 1 v2 legibility: join walked hashes back to their loaded
1306
- // records so each step in the walk carries a readable label
1307
- // instead of just a hash + distance. Builds the index once for
1308
- // O(N) lookup across the walk; for typical walks (depth=3, k<50)
1309
- // this is fast.
1310
- const byHash = new Map(loaded.map((lr) => [lr.record_hash, lr]));
1311
- const now = Date.now();
1312
- const enriched = walk.map((step) => {
1313
- // walk derives from graph (built from `loaded`); byHash always has a hit.
1314
- const lr = byHash.get(step.record_hash);
1315
- const ann = annotationsByRecord.get(step.record_hash);
1316
- return {
1317
- record_hash: step.record_hash,
1318
- distance: step.distance,
1319
- event_type: lr.record.event_type,
1320
- timestamp: lr.record.timestamp,
1321
- display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
1322
- display_producer: resolveDisplayProducer(lr.record, lr.producer),
1323
- age: formatAge(lr.record.timestamp, now),
1324
- };
1325
- });
1326
- return {
1327
- content: [
1328
- {
1329
- type: 'text',
1330
- text: JSON.stringify({
1331
- from_record_hash: args.from_record_hash,
1332
- edge_types: args.edge_types ?? [
1333
- 'CHAIN_PRECEDES',
1334
- 'INFORMED_BY',
1335
- 'ANNOTATES',
1336
- 'REVISES',
1337
- ],
1338
- depth,
1339
- count: enriched.length,
1340
- walk: enriched,
1341
- }, null, 2),
1342
- },
1343
- ],
1344
- };
1345
- }, 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));
1346
1590
  server.registerTool('recall_annotations', {
1347
- 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'.",
1348
1592
  inputSchema: {
1349
1593
  record_hash: z
1350
1594
  .string()
1351
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.'),
1352
1596
  },
1353
- }, async (args) => logReadPrimitiveCall('recall_annotations', args, async () => {
1354
- const { annotationsByRecord } = getLoadedMirrorSnapshot();
1355
- const summary = annotationsByRecord.get(args.record_hash) ?? null;
1356
- return {
1357
- content: [
1358
- {
1359
- type: 'text',
1360
- text: JSON.stringify({ record_hash: args.record_hash, annotations: summary }, null, 2),
1361
- },
1362
- ],
1363
- };
1364
- }, extractRecordHashFieldsFromMcpResult));
1597
+ }, async (args) => logReadPrimitiveCall('recall_annotations', args, async () => jsonToolResult(await runRecallAnnotations(args)), extractRecordHashFieldsFromMcpResult));
1365
1598
  server.registerTool('recall_revisions', {
1366
- 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'.",
1367
1600
  inputSchema: {
1368
1601
  record_hash: z
1369
1602
  .string()
1370
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).'),
1371
1604
  },
1372
- }, async (args) => logReadPrimitiveCall('recall_revisions', args, async () => {
1373
- const { loaded, revisionsByRecord } = getLoadedMirrorSnapshot();
1374
- const byHash = new Map();
1375
- for (const lr of loaded)
1376
- byHash.set(lr.record_hash, lr);
1377
- const chain = [];
1378
- const seen = new Set();
1379
- let current = args.record_hash;
1380
- while (!seen.has(current)) {
1381
- seen.add(current);
1382
- const next = revisionsByRecord.get(current);
1383
- if (!next || next.length === 0)
1384
- break;
1385
- // Each entry in the map's value array is a revision pointing at
1386
- // `current`. The chain follows the first-by-timestamp revision;
1387
- // the remaining entries are surfaced as `sibling_hashes` so the
1388
- // agent learns that branches exist without the chain shape
1389
- // having to explode into a tree.
1390
- const revHash = next[0];
1391
- const siblings = next.slice(1);
1392
- const revLr = byHash.get(revHash);
1393
- const entry = { record_hash: revHash };
1394
- if (revLr) {
1395
- entry.timestamp = revLr.record.timestamp;
1396
- const c = revLr.content;
1397
- if (c && typeof c === 'object' && !Array.isArray(c)) {
1398
- const cObj = c;
1399
- if (typeof cObj.new_position === 'string')
1400
- entry.new_position = cObj.new_position;
1401
- if (typeof cObj.reason === 'string')
1402
- entry.reason = cObj.reason;
1403
- if (typeof cObj.importance === 'string')
1404
- entry.importance = cObj.importance;
1405
- }
1406
- }
1407
- if (siblings.length > 0) {
1408
- entry.sibling_hashes = siblings;
1409
- }
1410
- chain.push(entry);
1411
- current = revHash;
1412
- }
1413
- return {
1414
- content: [
1415
- {
1416
- type: 'text',
1417
- text: JSON.stringify({ record_hash: args.record_hash, revision_chain: chain }, null, 2),
1418
- },
1419
- ],
1420
- };
1421
- }, extractRecordHashFieldsFromMcpResult));
1605
+ }, async (args) => logReadPrimitiveCall('recall_revisions', args, async () => jsonToolResult(await runRecallRevisions(args)), extractRecordHashFieldsFromMcpResult));
1422
1606
  server.registerTool('recall_by_content', {
1423
- 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'.",
1424
1608
  inputSchema: {
1425
1609
  query: z
1426
1610
  .string()
@@ -1445,129 +1629,14 @@ export function registerAtribRecallTools(server) {
1445
1629
  .optional()
1446
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.'),
1447
1631
  },
1448
- }, async (args) => logReadPrimitiveCall('recall_by_content', args, async () => {
1449
- const evidenceMode = args.evidence_mode === 'require_complete' ? 'require_complete' : 'bounded';
1450
- const includeToolCallArgs = args.include_tool_call_args === true;
1451
- const boundedLimit = resolveBoundedContentSearchLimit(args.max_records, Number.MAX_SAFE_INTEGER);
1452
- const snapshot = getContentSearchSnapshotForRecall(evidenceMode, boundedLimit);
1453
- const { entryByHash } = snapshot;
1454
- const totalRecords = snapshot.totalRecords;
1455
- const loadedLength = snapshot.entries.length;
1456
- const searchLimit = evidenceMode === 'require_complete'
1457
- ? resolveCompleteContentSearchLimit(args.max_records, loadedLength)
1458
- : Math.min(boundedLimit, loadedLength);
1459
- const explicitLimit = hasExplicitRecordLimit(args.max_records);
1460
- if (evidenceMode === 'require_complete' && explicitLimit && searchLimit < loadedLength) {
1461
- const k = Math.max(1, Math.min(50, args.k ?? 10));
1462
- return {
1463
- content: [
1464
- {
1465
- type: 'text',
1466
- text: JSON.stringify({
1467
- query: args.query,
1468
- k,
1469
- runtime: recallRuntimeMetadata(),
1470
- ...(includeToolCallArgs ? { include_tool_call_args: true } : {}),
1471
- evidence_mode: evidenceMode,
1472
- evidence_status: 'incomplete',
1473
- fallback_required: true,
1474
- fallback_reason: `require_complete refused a partial corpus: search_cap=${searchLimit}, ` +
1475
- `total_records=${loadedLength}.`,
1476
- fallback: 'Rerun without max_records for full loaded-mirror coverage, or pass an explicit ' +
1477
- 'partition filter through a caller-owned audit plan and treat each partition as its own coverage claim.',
1478
- total_records: loadedLength,
1479
- searched_records: 0,
1480
- search_cap: searchLimit,
1481
- coverage: recallCoverage(snapshot, 'incomplete_explicit_limit', 0, snapshot.index),
1482
- candidate_records: 0,
1483
- truncated_corpus: true,
1484
- count: 0,
1485
- results: [],
1486
- }, null, 2),
1487
- },
1488
- ],
1489
- };
1490
- }
1491
- const bm25Index = getContentBm25IndexForNewestLimit(snapshot, searchLimit);
1492
- const queryTokens = tokenize(args.query);
1493
- const relevanceByHash = bm25ScoresForQuery(bm25Index, queryTokens);
1494
- const now = Date.now();
1495
- const searchPool = queryTokens.length > 0
1496
- ? Array.from(relevanceByHash.keys())
1497
- .map((hash) => entryByHash.get(hash))
1498
- .filter((entry) => entry !== undefined)
1499
- : snapshot.newestEntries.slice(0, searchLimit);
1500
- const filteredSearchPool = searchPool.filter((entry) => !(entry.lifecycle_anchor && !queryMentionsLifecycle(queryTokens)));
1501
- const scored = filteredSearchPool.map((entry) => {
1502
- const ann = entry.annotations;
1503
- const suppressLifecycleAnchor = entry.lifecycle_anchor && !queryMentionsLifecycle(queryTokens);
1504
- const toolCallScoreFactor = effectiveToolCallScoreFactor(entry, includeToolCallArgs);
1505
- const r = recencyScore(entry.timestamp, now, ATRIB_RECALL_TAU_DAYS) * toolCallScoreFactor;
1506
- const i = suppressLifecycleAnchor ? 0 : importanceScore(ann);
1507
- const rawRel = !suppressLifecycleAnchor && queryTokens.length > 0
1508
- ? (relevanceByHash.get(entry.record_hash) ?? 0)
1509
- : 0;
1510
- const rel = !suppressLifecycleAnchor && queryTokens.length > 0
1511
- ? normalizedBm25Relevance(bm25Index, entry.record_hash, queryTokens, rawRel) *
1512
- toolCallScoreFactor
1513
- : 0;
1514
- const score = parkScore(r, i, rel, ATRIB_RECALL_ALPHA, ATRIB_RECALL_BETA, ATRIB_RECALL_GAMMA);
1515
- return { entry, score, recency: r, importance: i, relevance: rel };
1516
- });
1517
- scored.sort((a, b) => {
1518
- if (b.score !== a.score)
1519
- return b.score - a.score;
1520
- return b.entry.timestamp - a.entry.timestamp;
1521
- });
1522
- const k = Math.max(1, Math.min(50, args.k ?? 10));
1523
- const top = scored.slice(0, k);
1524
- const completeCoverage = !snapshot.partial && searchLimit >= loadedLength;
1525
- return {
1526
- content: [
1527
- {
1528
- type: 'text',
1529
- text: JSON.stringify({
1530
- query: args.query,
1531
- k,
1532
- runtime: recallRuntimeMetadata(),
1533
- ...(includeToolCallArgs ? { include_tool_call_args: true } : {}),
1534
- evidence_mode: evidenceMode,
1535
- evidence_status: completeCoverage ? 'complete' : 'bounded',
1536
- fallback_required: false,
1537
- total_records: totalRecords,
1538
- searched_records: searchLimit,
1539
- coverage: recallCoverage(snapshot, completeCoverage ? 'complete_full_scan' : 'bounded_newest_first', searchLimit, snapshot.index),
1540
- candidate_records: filteredSearchPool.length,
1541
- truncated_corpus: !completeCoverage,
1542
- count: top.length,
1543
- results: top.map(({ entry, score, recency, importance, relevance }) => {
1544
- return {
1545
- record_hash: entry.record_hash,
1546
- event_type: entry.event_type,
1547
- context_id: entry.context_id,
1548
- timestamp: entry.timestamp,
1549
- tool_name: entry.tool_name,
1550
- annotations: entry.annotations,
1551
- // Layer 1 v2 legibility fields (parity with compactify).
1552
- display_summary: entry.display_summary,
1553
- display_producer: entry.display_producer,
1554
- age: formatAge(entry.timestamp, now),
1555
- score,
1556
- components: { recency, importance, relevance },
1557
- };
1558
- }),
1559
- }, null, 2),
1560
- },
1561
- ],
1562
- };
1563
- }, extractRecordHashFieldsFromMcpResult));
1632
+ }, async (args) => logReadPrimitiveCall('recall_by_content', args, async () => jsonToolResult(await runRecallByContent(args)), extractRecordHashFieldsFromMcpResult));
1564
1633
  // recall_session_chain: records in a context_id, ordered chronologically
1565
1634
  // (the natural traversal of the CHAIN_PRECEDES topology). Doable via
1566
1635
  // recall_my_attribution_history({context_id}) + a manual timestamp sort,
1567
1636
  // but agents naturally think "what happened in this session?" and this
1568
1637
  // gives them one call.
1569
1638
  server.registerTool('recall_session_chain', {
1570
- 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'.",
1571
1640
  inputSchema: {
1572
1641
  context_id: z
1573
1642
  .string()
@@ -1582,81 +1651,11 @@ export function registerAtribRecallTools(server) {
1582
1651
  .optional()
1583
1652
  .describe('When true, include D062 local mirror body as local_content on each returned record. Defaults false to keep the session chain cheap.'),
1584
1653
  },
1585
- }, async (args) => logReadPrimitiveCall('recall_session_chain', args, async () => {
1586
- const ctx = args.context_id ?? resolveEnvContextId();
1587
- if (!ctx) {
1588
- return {
1589
- content: [
1590
- {
1591
- type: 'text',
1592
- text: JSON.stringify({
1593
- context_id: null,
1594
- count: 0,
1595
- records: [],
1596
- warning: 'no context_id supplied or resolvable via env',
1597
- }, null, 2),
1598
- },
1599
- ],
1600
- };
1601
- }
1602
- const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
1603
- const filtered = loaded
1604
- .filter((lr) => lr.record.context_id === ctx)
1605
- .sort((a, b) => a.record.timestamp - b.record.timestamp);
1606
- const limit = Math.max(1, Math.min(500, args.limit ?? 50));
1607
- const sliced = filtered.slice(0, limit);
1608
- const now = Date.now();
1609
- return {
1610
- content: [
1611
- {
1612
- type: 'text',
1613
- text: JSON.stringify({
1614
- context_id: ctx,
1615
- total: filtered.length,
1616
- returned: sliced.length,
1617
- truncated: filtered.length > sliced.length,
1618
- records: sliced.map((lr) => {
1619
- const ann = annotationsByRecord.get(lr.record_hash);
1620
- const entry = {
1621
- record_hash: lr.record_hash,
1622
- event_type: lr.record.event_type,
1623
- timestamp: lr.record.timestamp,
1624
- display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
1625
- display_producer: resolveDisplayProducer(lr.record, lr.producer),
1626
- age: formatAge(lr.record.timestamp, now),
1627
- };
1628
- const informedBy = lr.record
1629
- .informed_by;
1630
- const toolName = lr.record.tool_name;
1631
- const argsHash = lr.record.args_hash;
1632
- const resultHash = lr.record
1633
- .result_hash;
1634
- if (Array.isArray(informedBy) && informedBy.length > 0) {
1635
- entry.informed_by = informedBy;
1636
- }
1637
- if (toolName)
1638
- entry.tool_name = toolName;
1639
- if (argsHash)
1640
- entry.args_hash = argsHash;
1641
- if (resultHash)
1642
- entry.result_hash = resultHash;
1643
- if (args.include_content === true && lr.content !== undefined) {
1644
- entry.local_content = lr.content;
1645
- }
1646
- if (args.include_content === true && lr.producer !== undefined) {
1647
- entry.local_producer = lr.producer;
1648
- }
1649
- return entry;
1650
- }),
1651
- }, null, 2),
1652
- },
1653
- ],
1654
- };
1655
- }, extractRecordHashFieldsFromMcpResult));
1654
+ }, async (args) => logReadPrimitiveCall('recall_session_chain', args, async () => jsonToolResult(await runRecallSessionChain(args)), extractRecordHashFieldsFromMcpResult));
1656
1655
  // recall_orphans: records nothing else cites via informed_by. Useful for
1657
1656
  // "what decisions did I make and never act on?": loose-end discovery.
1658
1657
  server.registerTool('recall_orphans', {
1659
- 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'.",
1660
1659
  inputSchema: {
1661
1660
  context_id: z
1662
1661
  .string()
@@ -1672,119 +1671,47 @@ export function registerAtribRecallTools(server) {
1672
1671
  .optional()
1673
1672
  .describe('Maximum records to return (default 50, max 500), newest-first.'),
1674
1673
  },
1675
- }, async (args) => logReadPrimitiveCall('recall_orphans', args, async () => {
1676
- const { loaded, annotationsByRecord } = getLoadedMirrorSnapshot();
1677
- // Build the set of all record_hashes that appear in any record's
1678
- // informed_by field. Anything in `loaded` whose record_hash is
1679
- // NOT in this set is an orphan.
1680
- const cited = new Set();
1681
- for (const lr of loaded) {
1682
- const ib = lr.record.informed_by;
1683
- if (Array.isArray(ib)) {
1684
- for (const h of ib)
1685
- if (typeof h === 'string')
1686
- cited.add(h);
1687
- }
1688
- }
1689
- let orphans = loaded.filter((lr) => !cited.has(lr.record_hash));
1690
- if (args.context_id) {
1691
- orphans = orphans.filter((lr) => lr.record.context_id === args.context_id);
1692
- }
1693
- if (args.creator_key) {
1694
- orphans = orphans.filter((lr) => lr.record.creator_key === args.creator_key);
1695
- }
1696
- if (args.event_type) {
1697
- const targetUri = normalizeEventType(args.event_type);
1698
- orphans = orphans.filter((lr) => lr.record.event_type === targetUri);
1699
- }
1700
- orphans.sort((a, b) => b.record.timestamp - a.record.timestamp);
1701
- const limit = Math.max(1, Math.min(500, args.limit ?? 50));
1702
- const sliced = orphans.slice(0, limit);
1703
- const now = Date.now();
1704
- return {
1705
- content: [
1706
- {
1707
- type: 'text',
1708
- text: JSON.stringify({
1709
- total: orphans.length,
1710
- returned: sliced.length,
1711
- truncated: orphans.length > sliced.length,
1712
- records: sliced.map((lr) => {
1713
- const ann = annotationsByRecord.get(lr.record_hash);
1714
- return {
1715
- record_hash: lr.record_hash,
1716
- event_type: lr.record.event_type,
1717
- context_id: lr.record.context_id,
1718
- timestamp: lr.record.timestamp,
1719
- display_summary: synthesizeDisplaySummary(lr.record, lr.content, ann),
1720
- display_producer: resolveDisplayProducer(lr.record, lr.producer),
1721
- age: formatAge(lr.record.timestamp, now),
1722
- };
1723
- }),
1724
- }, null, 2),
1725
- },
1726
- ],
1727
- };
1728
- }, extractRecordHashFieldsFromMcpResult));
1674
+ }, async (args) => logReadPrimitiveCall('recall_orphans', args, async () => jsonToolResult(await runRecallOrphans(args)), extractRecordHashFieldsFromMcpResult));
1729
1675
  // recall_by_signer: aggregate the mirror by creator_key. Lets the agent
1730
1676
  // ask "who else is in this mirror?" before deciding whether to scope
1731
1677
  // queries with creator_key filters. Useful when the mirror is shared
1732
1678
  // across multi-agent flows (transactions with counterparty signers,
1733
1679
  // peer-shared records, etc.).
1734
1680
  server.registerTool('recall_by_signer', {
1735
- 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'.",
1736
1682
  inputSchema: {
1737
1683
  min_records: z
1738
1684
  .number()
1739
1685
  .optional()
1740
1686
  .describe('Optional minimum record count to include a creator in the result. Default 1 (include all).'),
1741
1687
  },
1742
- }, async (args) => logReadPrimitiveCall('recall_by_signer', args, async () => {
1743
- const { loaded } = getLoadedMirrorSnapshot();
1744
- const byKey = new Map();
1745
- for (const lr of loaded) {
1746
- const key = lr.record.creator_key;
1747
- const existing = byKey.get(key);
1748
- if (existing) {
1749
- existing.count++;
1750
- if (lr.record.timestamp > existing.latest_timestamp)
1751
- existing.latest_timestamp = lr.record.timestamp;
1752
- if (lr.record.timestamp < existing.earliest_timestamp)
1753
- existing.earliest_timestamp = lr.record.timestamp;
1754
- }
1755
- else {
1756
- byKey.set(key, {
1757
- creator_key: key,
1758
- count: 1,
1759
- latest_timestamp: lr.record.timestamp,
1760
- earliest_timestamp: lr.record.timestamp,
1761
- });
1762
- }
1763
- }
1764
- const minRecords = Math.max(1, args.min_records ?? 1);
1765
- const stats = [...byKey.values()]
1766
- .filter((s) => s.count >= minRecords)
1767
- .sort((a, b) => b.count - a.count);
1768
- return {
1769
- content: [
1770
- {
1771
- type: 'text',
1772
- text: JSON.stringify({
1773
- total_signers: stats.length,
1774
- total_records: loaded.length,
1775
- signers: stats,
1776
- }, null, 2),
1777
- },
1778
- ],
1779
- };
1780
- }, 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);
1781
1708
  }
1782
1709
  export function createAtribRecallServer() {
1783
1710
  const mcp = new McpServer({
1784
1711
  name: 'atrib-recall',
1785
1712
  version: readPackageVersion(),
1786
1713
  });
1787
- registerAtribRecallTools(mcp);
1714
+ registerAtribReadTools(mcp);
1788
1715
  return { mcp };
1789
1716
  }
1790
1717
  async function main() {