iranti 0.3.0 → 0.3.2

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.
Files changed (59) hide show
  1. package/README.md +843 -839
  2. package/bin/iranti.js +1 -1
  3. package/dist/scripts/claude-code-memory-hook.js +41 -153
  4. package/dist/scripts/iranti-cli.js +86 -9
  5. package/dist/scripts/iranti-mcp.js +141 -69
  6. package/dist/scripts/seed.js +1 -1
  7. package/dist/src/api/middleware/validation.d.ts.map +1 -1
  8. package/dist/src/api/middleware/validation.js +13 -1
  9. package/dist/src/api/middleware/validation.js.map +1 -1
  10. package/dist/src/api/routes/knowledge.d.ts.map +1 -1
  11. package/dist/src/api/routes/knowledge.js +3 -0
  12. package/dist/src/api/routes/knowledge.js.map +1 -1
  13. package/dist/src/api/routes/memory.d.ts.map +1 -1
  14. package/dist/src/api/routes/memory.js +3 -0
  15. package/dist/src/api/routes/memory.js.map +1 -1
  16. package/dist/src/api/server.js +1 -1
  17. package/dist/src/archivist/index.js +9 -9
  18. package/dist/src/attendant/AttendantInstance.d.ts +42 -1
  19. package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
  20. package/dist/src/attendant/AttendantInstance.js +496 -90
  21. package/dist/src/attendant/AttendantInstance.js.map +1 -1
  22. package/dist/src/attendant/index.d.ts +1 -1
  23. package/dist/src/attendant/index.d.ts.map +1 -1
  24. package/dist/src/attendant/index.js.map +1 -1
  25. package/dist/src/chat/index.d.ts +2 -0
  26. package/dist/src/chat/index.d.ts.map +1 -1
  27. package/dist/src/chat/index.js +56 -22
  28. package/dist/src/chat/index.js.map +1 -1
  29. package/dist/src/lib/assistantCheckpoint.d.ts +21 -0
  30. package/dist/src/lib/assistantCheckpoint.d.ts.map +1 -0
  31. package/dist/src/lib/assistantCheckpoint.js +143 -0
  32. package/dist/src/lib/assistantCheckpoint.js.map +1 -0
  33. package/dist/src/lib/cliHelpCatalog.js +2 -2
  34. package/dist/src/lib/cliHelpCatalog.js.map +1 -1
  35. package/dist/src/lib/hostMemoryFormatting.d.ts +25 -0
  36. package/dist/src/lib/hostMemoryFormatting.d.ts.map +1 -0
  37. package/dist/src/lib/hostMemoryFormatting.js +55 -0
  38. package/dist/src/lib/hostMemoryFormatting.js.map +1 -0
  39. package/dist/src/lib/projectLearning.d.ts +21 -0
  40. package/dist/src/lib/projectLearning.d.ts.map +1 -0
  41. package/dist/src/lib/projectLearning.js +357 -0
  42. package/dist/src/lib/projectLearning.js.map +1 -0
  43. package/dist/src/lib/protocolEnforcement.d.ts +3 -1
  44. package/dist/src/lib/protocolEnforcement.d.ts.map +1 -1
  45. package/dist/src/lib/protocolEnforcement.js +28 -2
  46. package/dist/src/lib/protocolEnforcement.js.map +1 -1
  47. package/dist/src/lib/sessionLedger.d.ts +18 -0
  48. package/dist/src/lib/sessionLedger.d.ts.map +1 -1
  49. package/dist/src/lib/sessionLedger.js +78 -0
  50. package/dist/src/lib/sessionLedger.js.map +1 -1
  51. package/dist/src/librarian/index.d.ts.map +1 -1
  52. package/dist/src/librarian/index.js +102 -51
  53. package/dist/src/librarian/index.js.map +1 -1
  54. package/dist/src/library/queries.js +56 -56
  55. package/dist/src/sdk/index.d.ts +2 -0
  56. package/dist/src/sdk/index.d.ts.map +1 -1
  57. package/dist/src/sdk/index.js +39 -2
  58. package/dist/src/sdk/index.js.map +1 -1
  59. package/package.json +9 -5
@@ -5,6 +5,7 @@ exports.normalizeExplicitTask = normalizeExplicitTask;
5
5
  exports.formatOperatingRulesText = formatOperatingRulesText;
6
6
  exports.readPersistedSessionState = readPersistedSessionState;
7
7
  exports.summarizeSessionState = summarizeSessionState;
8
+ const crypto_1 = require("crypto");
8
9
  const router_1 = require("../lib/router");
9
10
  const staffEventRegistry_1 = require("../lib/staffEventRegistry");
10
11
  const queries_1 = require("../library/queries");
@@ -19,6 +20,7 @@ const autoRemember_1 = require("../lib/autoRemember");
19
20
  const semanticFactTags_1 = require("../lib/semanticFactTags");
20
21
  const sessionLedger_1 = require("../lib/sessionLedger");
21
22
  const sharedStateInvalidation_1 = require("../lib/sharedStateInvalidation");
23
+ const hostMemoryFormatting_1 = require("../lib/hostMemoryFormatting");
22
24
  // ─── Constants ───────────────────────────────────────────────────────────────
23
25
  const ATTENDANT_RULES_QUERY = {
24
26
  entityType: 'system',
@@ -55,6 +57,11 @@ const ENTITY_DETECTION_WINDOW_CHARS = 1500;
55
57
  const MIN_ENTITY_CONFIDENCE = 0.75;
56
58
  const MEMORY_DECISION_CONTEXT_WINDOW_CHARS = 2000;
57
59
  const LEDGER_WORKING_MEMORY_PREFIX = 'system/session_ledger/recent_learning_';
60
+ const LEGACY_CONTINUITY_KEY_MAP = {
61
+ checkpoint_current_step: 'current_step',
62
+ checkpoint_next_step: 'next_step',
63
+ checkpoint_open_risks: 'open_risks',
64
+ };
58
65
  const ATTEND_EXPECTED_CALL_SEQUENCE = [
59
66
  'Call iranti_handshake at session start and again after context compaction.',
60
67
  "Call iranti_attend(phase='pre-response') before replying to the user.",
@@ -67,6 +74,20 @@ const ATTEND_EXPECTED_CALL_SEQUENCE = [
67
74
  ];
68
75
  const ATTEND_USAGE_REMINDER = 'Iranti is a hive mind. iranti_attend is mandatory before each reply and around knowledge discovery, and knowledge-changing actions must leave breadcrumbs through iranti_write and/or iranti_checkpoint so later sessions do not have to rediscover context.';
69
76
  const OBSERVE_USAGE_NOTE = 'observe() is retrieval-only. It surfaces candidate facts for context and warm-up, but it does not persist memory, replace iranti_attend, or count as a checkpoint/write.';
77
+ function normalizeContinuityKey(key) {
78
+ return LEGACY_CONTINUITY_KEY_MAP[key] ?? key;
79
+ }
80
+ function expandContinuityPriorityKeys(keys) {
81
+ const expanded = new Set();
82
+ for (const rawKey of keys) {
83
+ const key = rawKey.trim();
84
+ if (!key)
85
+ continue;
86
+ expanded.add(key);
87
+ expanded.add(normalizeContinuityKey(key));
88
+ }
89
+ return Array.from(expanded);
90
+ }
70
91
  const MEMORY_NEED_POSITIVE_PATTERNS = [
71
92
  /\bwhat(?:'s| is| was)?\s+my\b/i,
72
93
  /\bdo you remember\b/i,
@@ -213,6 +234,83 @@ function mergeWorkingMemoryWithLedger(entries, learnings) {
213
234
  ? [...retained, ...toLedgerWorkingMemoryEntries(learnings)]
214
235
  : retained;
215
236
  }
237
+ function normalizeProjectPolicyRuleLines(value, fallbackSummary) {
238
+ const rules = [];
239
+ if (typeof value === 'string' && value.trim()) {
240
+ rules.push(value.trim());
241
+ }
242
+ else if (value && typeof value === 'object') {
243
+ const record = value;
244
+ if (typeof record.rule === 'string' && record.rule.trim()) {
245
+ rules.push(record.rule.trim());
246
+ }
247
+ if (typeof record.text === 'string' && record.text.trim()) {
248
+ rules.push(record.text.trim());
249
+ }
250
+ if (typeof record.instruction === 'string' && record.instruction.trim()) {
251
+ rules.push(record.instruction.trim());
252
+ }
253
+ if (Array.isArray(record.rules)) {
254
+ for (const rule of record.rules) {
255
+ if (typeof rule === 'string' && rule.trim()) {
256
+ rules.push(rule.trim());
257
+ }
258
+ }
259
+ }
260
+ if (Array.isArray(record.preferences)) {
261
+ for (const preference of record.preferences) {
262
+ if (typeof preference === 'string' && preference.trim()) {
263
+ rules.push(preference.trim());
264
+ }
265
+ }
266
+ }
267
+ }
268
+ if (rules.length === 0 && fallbackSummary?.trim()) {
269
+ rules.push(fallbackSummary.trim());
270
+ }
271
+ return Array.from(new Set(rules.map((rule) => rule.trim()).filter(Boolean)));
272
+ }
273
+ function isProjectPolicyKey(key) {
274
+ return /(?:^agent_(?:operating_)?(?:rule|rules|preference|preferences)$|(?:_rule|_rules|_preference|_preferences)$)/i.test(key.trim());
275
+ }
276
+ function isProjectPolicyEntry(entry) {
277
+ if (isProjectPolicyKey(entry.key))
278
+ return true;
279
+ const durableClass = typeof entry.properties?.durableClass === 'string'
280
+ ? entry.properties.durableClass.trim().toLowerCase()
281
+ : '';
282
+ const semanticIntent = typeof entry.properties?.semanticIntent === 'string'
283
+ ? entry.properties.semanticIntent.trim().toLowerCase()
284
+ : '';
285
+ return durableClass === 'preference' || semanticIntent === 'preference_capture';
286
+ }
287
+ function toProjectPolicyWorkingMemoryEntries(entries) {
288
+ return entries.map((entry) => ({
289
+ entityKey: entry.entityKey,
290
+ summary: `Project policy: ${entry.summary}`,
291
+ confidence: 100,
292
+ source: entry.source,
293
+ lastUpdated: entry.lastUpdated,
294
+ }));
295
+ }
296
+ function mergeWorkingMemoryWithProjectPolicies(entries, policies) {
297
+ const retained = entries.filter((entry) => !policies.some((policy) => policy.entityKey === entry.entityKey));
298
+ return policies.length > 0
299
+ ? [...toProjectPolicyWorkingMemoryEntries(policies), ...retained]
300
+ : retained;
301
+ }
302
+ function applyProjectPolicyOperatingRules(operatingRules, projectPolicies) {
303
+ let nextRules = operatingRules;
304
+ for (const policy of projectPolicies) {
305
+ for (const rule of policy.rules) {
306
+ const renderedRule = `PROJECT POLICY (${policy.key}): ${rule}`;
307
+ if (!nextRules.includes(renderedRule)) {
308
+ nextRules = `${nextRules}\n- ${renderedRule}`;
309
+ }
310
+ }
311
+ }
312
+ return nextRules;
313
+ }
216
314
  function formatMissingWriteCategories(categories) {
217
315
  const labelMap = {
218
316
  findings: 'what you found',
@@ -489,6 +587,18 @@ function buildSessionComplianceState(input) {
489
587
  requiredAction: 'Persist durable findings with iranti_write or iranti_checkpoint before the next turn if new knowledge, validation, or file changes occurred.',
490
588
  });
491
589
  }
590
+ if (input.consecutiveUnusedMemoryInjections > 0) {
591
+ const severity = input.consecutiveUnusedMemoryInjections >= 2 ? 'error' : 'warn';
592
+ issues.push({
593
+ code: 'ignored_injected_memory',
594
+ severity,
595
+ count: input.consecutiveUnusedMemoryInjections,
596
+ message: input.consecutiveUnusedMemoryInjections >= 2
597
+ ? `Injected memory has been surfaced and then ignored across ${input.consecutiveUnusedMemoryInjections} consecutive turns.`
598
+ : 'Injected memory was surfaced but the response did not use it in the previous turn.',
599
+ requiredAction: 'On the next turn, either answer from the injected facts directly or persist why the injected memory was insufficient before rediscovering the same state manually.',
600
+ });
601
+ }
492
602
  let status = 'healthy';
493
603
  if (issues.some((issue) => issue.severity === 'error')) {
494
604
  status = 'non_compliant';
@@ -501,10 +611,14 @@ function buildSessionComplianceState(input) {
501
611
  ? 'Lifecycle is currently in progress and waiting for a post-response attend.'
502
612
  : 'Lifecycle is currently compliant.'
503
613
  : status === 'degraded'
504
- ? 'Lifecycle is degraded: persistence breadcrumbs are lagging.'
614
+ ? input.consecutiveUnusedMemoryInjections > 0
615
+ ? 'Lifecycle is degraded: injected memory was surfaced but not used.'
616
+ : 'Lifecycle is degraded: persistence breadcrumbs are lagging.'
505
617
  : input.consecutivePreResponseWithoutPost > 0
506
618
  ? 'Lifecycle is non-compliant: the previous turn is still missing a post-response attend.'
507
- : 'Lifecycle is non-compliant: accountability breadcrumbs are missing.';
619
+ : input.consecutiveUnusedMemoryInjections > 0
620
+ ? 'Lifecycle is non-compliant: injected memory is being ignored instead of used or explicitly challenged.'
621
+ : 'Lifecycle is non-compliant: accountability breadcrumbs are missing.';
508
622
  return {
509
623
  status,
510
624
  summary,
@@ -513,6 +627,7 @@ function buildSessionComplianceState(input) {
513
627
  counters: {
514
628
  attendsWithoutPersist: input.attendsWithoutPersist,
515
629
  consecutivePreResponseWithoutPost: input.consecutivePreResponseWithoutPost,
630
+ consecutiveUnusedMemoryInjections: input.consecutiveUnusedMemoryInjections,
516
631
  pendingPostResponse,
517
632
  lastAttendPhase: input.lastAttendPhase ?? null,
518
633
  },
@@ -970,13 +1085,13 @@ async function persistSharedCheckpointBreadcrumbs(params) {
970
1085
  if (checkpoint.currentStep) {
971
1086
  await (0, librarian_1.librarianWrite)({
972
1087
  ...common,
973
- key: 'checkpoint_current_step',
1088
+ key: 'current_step',
974
1089
  valueRaw: { text: checkpoint.currentStep },
975
- valueSummary: truncate(`checkpoint current step is ${checkpoint.currentStep}`, 220),
1090
+ valueSummary: truncate(`current step is ${checkpoint.currentStep}`, 220),
976
1091
  properties: {
977
1092
  ...checkpointBaseProperties,
978
1093
  durableClass: 'current_step',
979
- canonicalKey: 'checkpoint_current_step',
1094
+ canonicalKey: 'current_step',
980
1095
  mergeStrategy: 'replace',
981
1096
  ...(0, semanticFactTags_1.buildSemanticFactTags)({
982
1097
  memoryScope: 'project',
@@ -989,19 +1104,32 @@ async function persistSharedCheckpointBreadcrumbs(params) {
989
1104
  expectedKeys.push({
990
1105
  entityType: resolved.entityType,
991
1106
  entityId: resolved.entityId,
992
- key: 'checkpoint_current_step',
1107
+ key: 'current_step',
993
1108
  });
994
1109
  }
995
1110
  if (checkpoint.nextStep) {
1111
+ const existingNextStep = await (0, queries_2.findEntry)({
1112
+ entityType: resolved.entityType,
1113
+ entityId: resolved.entityId,
1114
+ key: 'next_step',
1115
+ });
1116
+ const priorInstruction = existingNextStep?.valueRaw && typeof existingNextStep.valueRaw === 'object'
1117
+ ? existingNextStep.valueRaw.instruction
1118
+ : null;
1119
+ const mergedNextStep = typeof priorInstruction === 'string'
1120
+ && priorInstruction.trim().length > 0
1121
+ && priorInstruction.trim() !== checkpoint.nextStep.trim()
1122
+ ? `${checkpoint.nextStep}. Prior task step: ${priorInstruction.trim()}`
1123
+ : checkpoint.nextStep;
996
1124
  await (0, librarian_1.librarianWrite)({
997
1125
  ...common,
998
- key: 'checkpoint_next_step',
999
- valueRaw: { instruction: checkpoint.nextStep },
1000
- valueSummary: truncate(`checkpoint next step is ${checkpoint.nextStep}`, 220),
1126
+ key: 'next_step',
1127
+ valueRaw: { instruction: mergedNextStep },
1128
+ valueSummary: truncate(`next step is ${mergedNextStep}`, 220),
1001
1129
  properties: {
1002
1130
  ...checkpointBaseProperties,
1003
1131
  durableClass: 'next_step',
1004
- canonicalKey: 'checkpoint_next_step',
1132
+ canonicalKey: 'next_step',
1005
1133
  mergeStrategy: 'replace',
1006
1134
  ...(0, semanticFactTags_1.buildSemanticFactTags)({
1007
1135
  memoryScope: 'project',
@@ -1014,7 +1142,7 @@ async function persistSharedCheckpointBreadcrumbs(params) {
1014
1142
  expectedKeys.push({
1015
1143
  entityType: resolved.entityType,
1016
1144
  entityId: resolved.entityId,
1017
- key: 'checkpoint_next_step',
1145
+ key: 'next_step',
1018
1146
  });
1019
1147
  }
1020
1148
  if (Array.isArray(checkpoint.fileChanges) && checkpoint.fileChanges.length > 0) {
@@ -1082,13 +1210,13 @@ async function persistSharedCheckpointBreadcrumbs(params) {
1082
1210
  if (checkpoint.openRisks && checkpoint.openRisks.length > 0) {
1083
1211
  await (0, librarian_1.librarianWrite)({
1084
1212
  ...common,
1085
- key: 'checkpoint_open_risks',
1213
+ key: 'open_risks',
1086
1214
  valueRaw: { items: checkpoint.openRisks },
1087
- valueSummary: truncate(`checkpoint open risks include ${checkpoint.openRisks.join('; ')}`, 220),
1215
+ valueSummary: truncate(`open risks include ${checkpoint.openRisks.join('; ')}`, 220),
1088
1216
  properties: {
1089
1217
  ...checkpointBaseProperties,
1090
1218
  durableClass: 'open_risks',
1091
- canonicalKey: 'checkpoint_open_risks',
1219
+ canonicalKey: 'open_risks',
1092
1220
  mergeStrategy: 'replace',
1093
1221
  ...(0, semanticFactTags_1.buildSemanticFactTags)({
1094
1222
  memoryScope: 'project',
@@ -1101,7 +1229,7 @@ async function persistSharedCheckpointBreadcrumbs(params) {
1101
1229
  expectedKeys.push({
1102
1230
  entityType: resolved.entityType,
1103
1231
  entityId: resolved.entityId,
1104
- key: 'checkpoint_open_risks',
1232
+ key: 'open_risks',
1105
1233
  });
1106
1234
  }
1107
1235
  }
@@ -1200,6 +1328,7 @@ class AttendantInstance {
1200
1328
  this.contextCallCount = 0;
1201
1329
  this.attendsWithoutPersist = 0;
1202
1330
  this.consecutivePreResponseWithoutPost = 0;
1331
+ this.consecutiveUnusedMemoryInjections = 0;
1203
1332
  this.lastAttendPhase = undefined;
1204
1333
  this.complianceUpdatedAt = new Date().toISOString();
1205
1334
  this.sessionStarted = new Date().toISOString();
@@ -1208,6 +1337,7 @@ class AttendantInstance {
1208
1337
  this.eventHost = null;
1209
1338
  this.sharedStateObservedAt = null;
1210
1339
  this.pendingSharedStateInvalidations = new Map();
1340
+ this.pendingMemoryAttributions = [];
1211
1341
  this.agentId = agentId;
1212
1342
  (0, sharedStateInvalidation_1.registerSharedStateInvalidationObserver)(agentId, this);
1213
1343
  }
@@ -1232,6 +1362,156 @@ class AttendantInstance {
1232
1362
  ...(this.eventHost ? { host: this.eventHost } : {}),
1233
1363
  };
1234
1364
  }
1365
+ updateBriefPendingMemoryAttributions() {
1366
+ if (!this.brief)
1367
+ return;
1368
+ this.brief = {
1369
+ ...this.brief,
1370
+ pendingMemoryAttributions: this.pendingMemoryAttributions.map((entry) => ({ ...entry })),
1371
+ };
1372
+ }
1373
+ addPendingMemoryAttribution(input) {
1374
+ const attribution = {
1375
+ injectionId: (0, crypto_1.randomUUID)(),
1376
+ surfaced: true,
1377
+ used: false,
1378
+ helpful: false,
1379
+ status: 'pending',
1380
+ phase: input.phase,
1381
+ surfacedAt: new Date().toISOString(),
1382
+ reason: 'awaiting_post_response_evaluation',
1383
+ injectedKeys: [...input.injectedKeys],
1384
+ injectedEntryIds: [...input.injectedEntryIds],
1385
+ evidenceKinds: [],
1386
+ };
1387
+ this.pendingMemoryAttributions.push(attribution);
1388
+ this.updateBriefPendingMemoryAttributions();
1389
+ return attribution;
1390
+ }
1391
+ recordMemoryEvidence(kind) {
1392
+ if (this.pendingMemoryAttributions.length === 0) {
1393
+ return;
1394
+ }
1395
+ for (const attribution of this.pendingMemoryAttributions) {
1396
+ if (attribution.status !== 'pending')
1397
+ continue;
1398
+ if (!attribution.evidenceKinds.includes(kind)) {
1399
+ attribution.evidenceKinds = [...attribution.evidenceKinds, kind];
1400
+ }
1401
+ (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
1402
+ staffComponent: 'Attendant',
1403
+ actionType: 'memory_evidence_observed',
1404
+ agentId: this.agentId,
1405
+ source: this.eventSource,
1406
+ reason: kind,
1407
+ level: 'audit',
1408
+ metadata: this.buildEventMetadata({
1409
+ injectionId: attribution.injectionId,
1410
+ injectedKeys: attribution.injectedKeys,
1411
+ injectedEntryIds: attribution.injectedEntryIds,
1412
+ evidenceKind: kind,
1413
+ }),
1414
+ });
1415
+ }
1416
+ this.updateBriefPendingMemoryAttributions();
1417
+ }
1418
+ responseMentionsInjectedMemory(response, attribution) {
1419
+ const responseTokens = new Set(tokenize(response));
1420
+ if (responseTokens.size === 0)
1421
+ return false;
1422
+ for (const entityKey of attribution.injectedKeys) {
1423
+ const key = entityKey.split('/').slice(2).join('/');
1424
+ for (const token of tokenize(key.replace(/[_/.-]+/g, ' '))) {
1425
+ if (responseTokens.has(token)) {
1426
+ return true;
1427
+ }
1428
+ }
1429
+ }
1430
+ return false;
1431
+ }
1432
+ responseShowsRecoveryValue(response, attribution) {
1433
+ const normalized = normalizeText(response);
1434
+ if (!normalized)
1435
+ return false;
1436
+ return attribution.injectedKeys.some((entityKey) => {
1437
+ const key = entityKey.split('/').slice(2).join('/');
1438
+ return (/\b(next step|current step|blocker|blockers|risk|risks|status|progress|file|files|changed|handoff|resume|recovery)\b/.test(normalized)
1439
+ && /\b(next_step|current_step|open_risks|status|checkpoint_summary|recent_file_changes|recent_actions|implementation_status|blockers?)\b/i.test(key));
1440
+ });
1441
+ }
1442
+ scorePendingMemoryAttributions(response) {
1443
+ if (this.pendingMemoryAttributions.length === 0) {
1444
+ return [];
1445
+ }
1446
+ const scoredAt = new Date().toISOString();
1447
+ const scored = this.pendingMemoryAttributions.map((entry) => {
1448
+ const evidenceKinds = [...entry.evidenceKinds];
1449
+ const rediscoveredManually = evidenceKinds.includes('rediscovery');
1450
+ if (!rediscoveredManually && this.responseMentionsInjectedMemory(response, entry) && !evidenceKinds.includes('response_reference')) {
1451
+ evidenceKinds.push('response_reference');
1452
+ }
1453
+ if (!rediscoveredManually && this.responseShowsRecoveryValue(response, entry) && !evidenceKinds.includes('response_recovery')) {
1454
+ evidenceKinds.push('response_recovery');
1455
+ }
1456
+ const used = evidenceKinds.includes('write')
1457
+ || evidenceKinds.includes('checkpoint')
1458
+ || evidenceKinds.includes('response_reference')
1459
+ || evidenceKinds.includes('response_recovery');
1460
+ const helpful = evidenceKinds.includes('checkpoint')
1461
+ || evidenceKinds.includes('write')
1462
+ || evidenceKinds.includes('response_recovery');
1463
+ const reason = helpful
1464
+ ? 'response_or_action_confirmed_memory_helpfulness'
1465
+ : used
1466
+ ? 'response_referenced_injected_memory'
1467
+ : 'memory_was_only_surfaced';
1468
+ const scoredEntry = {
1469
+ ...entry,
1470
+ used,
1471
+ helpful,
1472
+ status: 'scored',
1473
+ scoredAt,
1474
+ reason,
1475
+ evidenceKinds,
1476
+ };
1477
+ (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
1478
+ staffComponent: 'Attendant',
1479
+ actionType: 'memory_injection_scored',
1480
+ agentId: this.agentId,
1481
+ source: this.eventSource,
1482
+ reason,
1483
+ level: 'audit',
1484
+ metadata: this.buildEventMetadata({
1485
+ injectionId: scoredEntry.injectionId,
1486
+ surfaced: true,
1487
+ used,
1488
+ helpful,
1489
+ phase: scoredEntry.phase,
1490
+ injectedKeys: scoredEntry.injectedKeys,
1491
+ injectedEntryIds: scoredEntry.injectedEntryIds,
1492
+ evidenceKinds,
1493
+ scoredAt,
1494
+ }),
1495
+ });
1496
+ return scoredEntry;
1497
+ });
1498
+ if (scored.some((entry) => entry.used)) {
1499
+ this.consecutiveUnusedMemoryInjections = 0;
1500
+ }
1501
+ else if (scored.some((entry) => entry.surfaced)) {
1502
+ this.consecutiveUnusedMemoryInjections += 1;
1503
+ }
1504
+ this.pendingMemoryAttributions = [];
1505
+ this.updateBriefPendingMemoryAttributions();
1506
+ return scored;
1507
+ }
1508
+ async noteDiscoveryOccurred() {
1509
+ if (this.pendingMemoryAttributions.length === 0) {
1510
+ return;
1511
+ }
1512
+ this.recordMemoryEvidence('rediscovery');
1513
+ await this.persistState();
1514
+ }
1235
1515
  async loadSessionLedgerSignals(taskType) {
1236
1516
  try {
1237
1517
  const source = this.eventSource === 'internal' ? undefined : this.eventSource;
@@ -1260,6 +1540,33 @@ class AttendantInstance {
1260
1540
  return { learnings: [], profile: null };
1261
1541
  }
1262
1542
  }
1543
+ async loadProjectPolicies() {
1544
+ const configured = (0, autoRemember_1.getProjectMemoryEntity)();
1545
+ if (!configured)
1546
+ return [];
1547
+ const parsed = (0, entity_resolution_1.parseEntityString)(configured);
1548
+ const entries = await (0, queries_1.findEntriesByEntity)(parsed.entityType, parsed.entityId);
1549
+ const policies = entries
1550
+ .filter((entry) => isProjectPolicyEntry({
1551
+ key: entry.key,
1552
+ properties: entry.properties ?? null,
1553
+ }))
1554
+ .map((entry) => {
1555
+ const rules = normalizeProjectPolicyRuleLines(entry.valueRaw, entry.valueSummary);
1556
+ if (rules.length === 0)
1557
+ return null;
1558
+ return {
1559
+ entityKey: `${entry.entityType}/${entry.entityId}/${entry.key}`,
1560
+ summary: rules.join(' '),
1561
+ key: entry.key,
1562
+ source: entry.source,
1563
+ lastUpdated: entry.updatedAt.toISOString(),
1564
+ rules,
1565
+ };
1566
+ })
1567
+ .filter((entry) => Boolean(entry));
1568
+ return policies;
1569
+ }
1263
1570
  // ── Handshake ────────────────────────────────────────────────────────────
1264
1571
  async handshake(context) {
1265
1572
  const t0 = (0, metrics_1.timeStart)();
@@ -1271,13 +1578,15 @@ class AttendantInstance {
1271
1578
  // Infer task type
1272
1579
  const inferredTaskType = await this.inferTask(context);
1273
1580
  // Load knowledge — agent entries + related entities
1274
- const [workingMemory, ledgerSignals] = await Promise.all([
1581
+ const [workingMemory, ledgerSignals, projectPolicies] = await Promise.all([
1275
1582
  this.buildWorkingMemory(inferredTaskType),
1276
1583
  this.loadSessionLedgerSignals(inferredTaskType),
1584
+ this.loadProjectPolicies(),
1277
1585
  ]);
1278
1586
  const sessionLedgerLearnings = ledgerSignals.learnings;
1279
1587
  this.advisoryLearningProfile = ledgerSignals.profile;
1280
- const workingMemoryWithLedger = mergeWorkingMemoryWithLedger(workingMemory, sessionLedgerLearnings);
1588
+ const workingMemoryWithPolicies = mergeWorkingMemoryWithProjectPolicies(workingMemory, projectPolicies);
1589
+ const workingMemoryWithLedger = mergeWorkingMemoryWithLedger(workingMemoryWithPolicies, sessionLedgerLearnings);
1281
1590
  const recoveryResult = persisted?.sessionCheckpoint
1282
1591
  ? this.buildRecovery(context, persisted.sessionCheckpoint)
1283
1592
  : { interrupted: false, recovery: null };
@@ -1294,9 +1603,10 @@ class AttendantInstance {
1294
1603
  }
1295
1604
  this.brief = {
1296
1605
  agentId: this.agentId,
1297
- operatingRules: applyAdvisoryOperatingRules(operatingRules, this.advisoryLearningProfile),
1606
+ operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(operatingRules, projectPolicies), this.advisoryLearningProfile),
1298
1607
  inferredTaskType,
1299
1608
  workingMemory: workingMemoryWithLedger,
1609
+ projectPolicies,
1300
1610
  sessionStarted: persisted?.sessionStarted ?? this.sessionStarted,
1301
1611
  briefGeneratedAt: new Date().toISOString(),
1302
1612
  contextCallCount: this.contextCallCount,
@@ -1323,6 +1633,7 @@ class AttendantInstance {
1323
1633
  metadata: this.buildEventMetadata({
1324
1634
  briefSize: this.brief?.workingMemory.length ?? 0,
1325
1635
  ledgerLearningCount: sessionLedgerLearnings.length,
1636
+ projectPolicyCount: projectPolicies.length,
1326
1637
  advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
1327
1638
  taskSummary: context.task.slice(0, 120),
1328
1639
  }),
@@ -1340,7 +1651,10 @@ class AttendantInstance {
1340
1651
  return result;
1341
1652
  }
1342
1653
  const newTaskType = await this.inferTask(context);
1343
- const ledgerSignals = await this.loadSessionLedgerSignals(newTaskType);
1654
+ const [ledgerSignals, projectPolicies] = await Promise.all([
1655
+ this.loadSessionLedgerSignals(newTaskType),
1656
+ this.loadProjectPolicies(),
1657
+ ]);
1344
1658
  this.advisoryLearningProfile = ledgerSignals.profile;
1345
1659
  // Task hasn't shifted — update timestamp only
1346
1660
  if (newTaskType.toLowerCase() === this.brief.inferredTaskType.toLowerCase()) {
@@ -1354,8 +1668,9 @@ class AttendantInstance {
1354
1668
  }
1355
1669
  this.brief = {
1356
1670
  ...this.brief,
1357
- operatingRules: applyAdvisoryOperatingRules(this.brief.operatingRules, this.advisoryLearningProfile),
1358
- workingMemory: mergeWorkingMemoryWithLedger(this.brief.workingMemory, ledgerSignals.learnings),
1671
+ operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(await this.loadOperatingRules(), projectPolicies), this.advisoryLearningProfile),
1672
+ workingMemory: mergeWorkingMemoryWithLedger(mergeWorkingMemoryWithProjectPolicies(this.brief.workingMemory, projectPolicies), ledgerSignals.learnings),
1673
+ projectPolicies,
1359
1674
  briefGeneratedAt: new Date().toISOString(),
1360
1675
  contextCallCount: this.contextCallCount,
1361
1676
  sessionLedgerLearnings: ledgerSignals.learnings,
@@ -1375,6 +1690,7 @@ class AttendantInstance {
1375
1690
  metadata: this.buildEventMetadata({
1376
1691
  briefSize: this.brief?.workingMemory.length ?? 0,
1377
1692
  contextCallCount: this.contextCallCount,
1693
+ projectPolicyCount: projectPolicies.length,
1378
1694
  advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
1379
1695
  }),
1380
1696
  });
@@ -1385,9 +1701,10 @@ class AttendantInstance {
1385
1701
  const workingMemory = await this.buildWorkingMemory(newTaskType);
1386
1702
  this.brief = {
1387
1703
  ...this.brief,
1388
- operatingRules: applyAdvisoryOperatingRules(this.brief.operatingRules, this.advisoryLearningProfile),
1704
+ operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(await this.loadOperatingRules(), projectPolicies), this.advisoryLearningProfile),
1389
1705
  inferredTaskType: newTaskType,
1390
- workingMemory: mergeWorkingMemoryWithLedger(workingMemory, ledgerSignals.learnings),
1706
+ workingMemory: mergeWorkingMemoryWithLedger(mergeWorkingMemoryWithProjectPolicies(workingMemory, projectPolicies), ledgerSignals.learnings),
1707
+ projectPolicies,
1391
1708
  briefGeneratedAt: new Date().toISOString(),
1392
1709
  contextCallCount: this.contextCallCount,
1393
1710
  sessionLedgerLearnings: ledgerSignals.learnings,
@@ -1407,6 +1724,7 @@ class AttendantInstance {
1407
1724
  metadata: this.buildEventMetadata({
1408
1725
  briefSize: this.brief?.workingMemory.length ?? 0,
1409
1726
  contextCallCount: this.contextCallCount,
1727
+ projectPolicyCount: projectPolicies.length,
1410
1728
  advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
1411
1729
  }),
1412
1730
  });
@@ -1434,8 +1752,10 @@ class AttendantInstance {
1434
1752
  const operatingRules = rulesResult.found && rulesResult.entry
1435
1753
  ? formatOperatingRulesText(rulesResult.entry.valueRaw, rulesResult.entry.valueSummary)
1436
1754
  : formatOperatingRulesText(null, 'Attendant operating rules:');
1755
+ const projectPolicies = this.brief?.projectPolicies ?? await this.loadProjectPolicies();
1437
1756
  if (this.brief) {
1438
- this.brief.operatingRules = operatingRules;
1757
+ this.brief.operatingRules = applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(operatingRules, projectPolicies), this.advisoryLearningProfile);
1758
+ this.brief.projectPolicies = projectPolicies;
1439
1759
  this.brief.contextCallCount = 0;
1440
1760
  }
1441
1761
  this.contextCallCount = 0;
@@ -1473,6 +1793,7 @@ class AttendantInstance {
1473
1793
  return buildSessionComplianceState({
1474
1794
  attendsWithoutPersist: this.attendsWithoutPersist,
1475
1795
  consecutivePreResponseWithoutPost: this.consecutivePreResponseWithoutPost,
1796
+ consecutiveUnusedMemoryInjections: this.consecutiveUnusedMemoryInjections,
1476
1797
  lastAttendPhase: this.lastAttendPhase,
1477
1798
  lastUpdated: lastUpdated ?? this.complianceUpdatedAt,
1478
1799
  });
@@ -1482,6 +1803,7 @@ class AttendantInstance {
1482
1803
  this.lastAttendPhase = undefined;
1483
1804
  this.consecutivePreResponseWithoutPost = 0;
1484
1805
  this.complianceUpdatedAt = new Date().toISOString();
1806
+ this.recordMemoryEvidence('write');
1485
1807
  if (!this.brief) {
1486
1808
  return;
1487
1809
  }
@@ -1497,6 +1819,7 @@ class AttendantInstance {
1497
1819
  this.lastAttendPhase = undefined;
1498
1820
  this.consecutivePreResponseWithoutPost = 0;
1499
1821
  this.complianceUpdatedAt = new Date().toISOString();
1822
+ this.recordMemoryEvidence('checkpoint');
1500
1823
  this.setLedgerContext(input.ledgerContext);
1501
1824
  const now = new Date().toISOString();
1502
1825
  if (!this.brief) {
@@ -1730,6 +2053,7 @@ class AttendantInstance {
1730
2053
  this.attendsWithoutPersist++;
1731
2054
  const phase = input.phase;
1732
2055
  let complianceWarning;
2056
+ const ignoredMemoryWarning = 'COMPLIANCE: injected memory was surfaced but not used. On the next turn, either answer from injected facts directly or persist why the injected memory was insufficient before rediscovering the same state manually.';
1733
2057
  if (phase === 'post-response') {
1734
2058
  // Correct post-response call — reset counters
1735
2059
  this.attendsWithoutPersist = 0;
@@ -1753,7 +2077,10 @@ class AttendantInstance {
1753
2077
  }
1754
2078
  }
1755
2079
  this.complianceUpdatedAt = new Date().toISOString();
1756
- const compliance = this.buildComplianceState(this.complianceUpdatedAt);
2080
+ let compliance = this.buildComplianceState(this.complianceUpdatedAt);
2081
+ if (!complianceWarning && compliance.issues.some((issue) => issue.code === 'ignored_injected_memory')) {
2082
+ complianceWarning = ignoredMemoryWarning;
2083
+ }
1757
2084
  if (!this.brief) {
1758
2085
  const bootstrapTask = buildAttendBootstrapTask(latestMessage, currentContext);
1759
2086
  const bootstrapBrief = await this.handshake({
@@ -1788,6 +2115,52 @@ class AttendantInstance {
1788
2115
  }),
1789
2116
  });
1790
2117
  }
2118
+ if (phase === 'post-response') {
2119
+ const memoryAttributions = this.scorePendingMemoryAttributions(latestMessage || currentContext);
2120
+ compliance = this.buildComplianceState(this.complianceUpdatedAt);
2121
+ if (memoryAttributions.some((entry) => !entry.used)) {
2122
+ complianceWarning = ignoredMemoryWarning;
2123
+ }
2124
+ if (this.brief) {
2125
+ this.brief = {
2126
+ ...this.brief,
2127
+ compliance,
2128
+ briefGeneratedAt: this.complianceUpdatedAt,
2129
+ };
2130
+ await this.persistState();
2131
+ }
2132
+ (0, metrics_1.timeEnd)('attendant.attend_ms', t0);
2133
+ return {
2134
+ shouldInject: false,
2135
+ reason: 'memory_not_needed',
2136
+ decision: {
2137
+ needed: false,
2138
+ confidence: 1,
2139
+ method: 'heuristic',
2140
+ explanation: 'post_response_closeout',
2141
+ },
2142
+ bootstrap,
2143
+ complianceWarning,
2144
+ compliance,
2145
+ memoryAttributions,
2146
+ usageGuidance: buildUsageGuidance('attend'),
2147
+ facts: [],
2148
+ entitiesDetected: [],
2149
+ alreadyPresent: 0,
2150
+ totalFound: 0,
2151
+ entitiesResolved: [],
2152
+ debug: {
2153
+ skipped: 'empty_context',
2154
+ contextLength: currentContext.length,
2155
+ detectionWindowChars: Math.min(currentContext.length, ENTITY_DETECTION_WINDOW_CHARS),
2156
+ detectedCandidates: 0,
2157
+ keptCandidates: 0,
2158
+ hintsProvided: effectiveEntityHints.length,
2159
+ hintsResolved: 0,
2160
+ dropped: [{ name: latestMessage || '(none)', reason: 'post_response_closeout' }],
2161
+ },
2162
+ };
2163
+ }
1791
2164
  let decision = await this.decideMemoryNeed({
1792
2165
  currentContext,
1793
2166
  latestMessage,
@@ -1847,6 +2220,7 @@ class AttendantInstance {
1847
2220
  bootstrap,
1848
2221
  complianceWarning,
1849
2222
  compliance,
2223
+ memoryAttributions: [],
1850
2224
  usageGuidance: buildUsageGuidance('attend'),
1851
2225
  facts: [],
1852
2226
  entitiesDetected: [],
@@ -1870,11 +2244,11 @@ class AttendantInstance {
1870
2244
  currentContext: observationContext,
1871
2245
  maxFacts: input.maxFacts,
1872
2246
  entityHints: observeEntityHints,
1873
- priorityKeys: Array.from(new Set([
2247
+ priorityKeys: expandContinuityPriorityKeys(Array.from(new Set([
1874
2248
  ...(mandatoryRecall.key ? [mandatoryRecall.key] : []),
1875
2249
  ...(this.advisoryLearningProfile?.priorityKeys ?? []),
1876
2250
  ...freshState.priorityKeys,
1877
- ])),
2251
+ ]))),
1878
2252
  skipContextFilter: forceInject,
1879
2253
  ledgerContext: input.ledgerContext,
1880
2254
  });
@@ -1891,10 +2265,11 @@ class AttendantInstance {
1891
2265
  const remainder = slashIdx2 === -1 ? '' : fact.entityKey.slice(slashIdx2);
1892
2266
  return { ...fact, entityKey: `${canonicalPersonalType}/${canonicalPersonalId}${remainder}` };
1893
2267
  });
2268
+ const structuredFacts = (0, hostMemoryFormatting_1.assignStructuredFactIds)(remappedFacts);
1894
2269
  watchedEntitiesChanged = this.updateWatchedEntities(observed.entitiesResolved?.map((entry) => entry.canonicalEntity) ?? []) || watchedEntitiesChanged;
1895
2270
  this.markSharedStateObserved(observeEntityHints.length > 0 ? observeEntityHints : freshState.entities);
1896
2271
  let reason = 'memory_needed_injected';
1897
- const shouldInject = remappedFacts.length > 0;
2272
+ const shouldInject = structuredFacts.length > 0;
1898
2273
  let searchSuggestion;
1899
2274
  if (!shouldInject) {
1900
2275
  const allAlreadyInContext = observed.totalFound > 0 && observed.alreadyPresent >= observed.totalFound;
@@ -1916,16 +2291,28 @@ class AttendantInstance {
1916
2291
  else if (forceInject) {
1917
2292
  reason = 'forced';
1918
2293
  }
2294
+ const memoryAttributions = shouldInject
2295
+ ? [
2296
+ this.addPendingMemoryAttribution({
2297
+ phase: phase === 'mid-turn' ? 'mid-turn' : 'pre-response',
2298
+ injectedKeys: structuredFacts.map((fact) => fact.entityKey),
2299
+ injectedEntryIds: structuredFacts
2300
+ .map((fact) => fact.knowledgeEntryId)
2301
+ .filter((value) => typeof value === 'number'),
2302
+ }),
2303
+ ]
2304
+ : [];
1919
2305
  const attendResult = {
1920
2306
  ...observed,
1921
- facts: remappedFacts,
1922
- shouldInject,
2307
+ facts: structuredFacts,
2308
+ shouldInject: structuredFacts.length > 0,
1923
2309
  reason,
1924
2310
  decision,
1925
2311
  bootstrap,
1926
2312
  searchSuggestion,
1927
2313
  complianceWarning,
1928
2314
  compliance,
2315
+ memoryAttributions,
1929
2316
  usageGuidance: buildUsageGuidance('attend'),
1930
2317
  };
1931
2318
  if (input.suppressEvents !== true) {
@@ -1940,6 +2327,7 @@ class AttendantInstance {
1940
2327
  contextCallCount: this.contextCallCount,
1941
2328
  shouldInject,
1942
2329
  attendReason: reason,
2330
+ injectionId: memoryAttributions[0]?.injectionId ?? null,
1943
2331
  advisoryDecisionMethod: decision.method,
1944
2332
  advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
1945
2333
  freshStateEntities: freshState.entities,
@@ -1956,7 +2344,11 @@ class AttendantInstance {
1956
2344
  metadata: this.buildEventMetadata({
1957
2345
  shouldInject,
1958
2346
  factCount: observed.facts.length,
2347
+ injectionId: memoryAttributions[0]?.injectionId ?? null,
1959
2348
  injectedKeys: observed.facts.map((fact) => fact.entityKey),
2349
+ injectedEntryIds: observed.facts
2350
+ .map((fact) => fact.knowledgeEntryId)
2351
+ .filter((value) => typeof value === 'number'),
1960
2352
  entitiesResolved: observed.entitiesResolved?.map((entry) => entry.canonicalEntity) ?? [],
1961
2353
  alreadyPresent: observed.alreadyPresent,
1962
2354
  totalFound: observed.totalFound,
@@ -1973,7 +2365,7 @@ class AttendantInstance {
1973
2365
  briefGeneratedAt: this.complianceUpdatedAt,
1974
2366
  };
1975
2367
  }
1976
- if (watchedEntitiesChanged || this.brief?.compliance !== compliance) {
2368
+ if (watchedEntitiesChanged || this.brief?.compliance !== compliance || memoryAttributions.length > 0) {
1977
2369
  await this.persistState();
1978
2370
  }
1979
2371
  (0, metrics_1.timeEnd)('attendant.attend_ms', t0);
@@ -2034,29 +2426,29 @@ class AttendantInstance {
2034
2426
  const entityResponse = await (0, router_1.route)('extraction', [
2035
2427
  {
2036
2428
  role: 'user',
2037
- content: `Extract explicitly named entities from the text.
2038
- An entity can be a person, organization, project, technology, or named concept.
2039
-
2040
- Return ONLY valid JSON as an array of objects in this exact shape:
2041
- [
2042
- {
2043
- "type": "project",
2044
- "name": "Project Atlas",
2045
- "id_guess": "project_atlas",
2046
- "confidence": 0.92,
2047
- "evidence": "Project Atlas",
2048
- "start": 123,
2049
- "end": 136
2050
- }
2051
- ]
2052
-
2053
- Rules:
2054
- - Only include entities explicitly named in the provided text.
2055
- - Do not infer or carry over entities not present in the text.
2056
- - If uncertain, omit.
2057
- - If none are present, return [].
2058
-
2059
- Text:
2429
+ content: `Extract explicitly named entities from the text.
2430
+ An entity can be a person, organization, project, technology, or named concept.
2431
+
2432
+ Return ONLY valid JSON as an array of objects in this exact shape:
2433
+ [
2434
+ {
2435
+ "type": "project",
2436
+ "name": "Project Atlas",
2437
+ "id_guess": "project_atlas",
2438
+ "confidence": 0.92,
2439
+ "evidence": "Project Atlas",
2440
+ "start": 123,
2441
+ "end": 136
2442
+ }
2443
+ ]
2444
+
2445
+ Rules:
2446
+ - Only include entities explicitly named in the provided text.
2447
+ - Do not infer or carry over entities not present in the text.
2448
+ - If uncertain, omit.
2449
+ - If none are present, return [].
2450
+
2451
+ Text:
2060
2452
  ${detectionWindow}`,
2061
2453
  },
2062
2454
  ], 512);
@@ -2253,7 +2645,7 @@ ${detectionWindow}`,
2253
2645
  const allEntries = await (0, queries_1.findEntriesByEntity)(resolvedInfo.entityType, resolvedInfo.entityId);
2254
2646
  // Priority keys first
2255
2647
  const policyPriorityKeys = policy.observeKeyPriority?.[resolvedInfo.entityType] ?? [];
2256
- const priorityKeys = new Set([...policyPriorityKeys, ...requestedPriorityKeys]);
2648
+ const priorityKeys = new Set(expandContinuityPriorityKeys([...policyPriorityKeys, ...requestedPriorityKeys]));
2257
2649
  const priorityEntries = allEntries.filter((e) => priorityKeys.has(e.key));
2258
2650
  const remainingEntries = allEntries
2259
2651
  .filter((e) => !priorityKeys.has(e.key))
@@ -2264,13 +2656,20 @@ ${detectionWindow}`,
2264
2656
  || a.key.localeCompare(b.key));
2265
2657
  });
2266
2658
  const selectedEntries = [...priorityEntries, ...remainingEntries].slice(0, maxKeysPerEntity);
2659
+ const freshestEntry = allEntries
2660
+ .slice()
2661
+ .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() || b.confidence - a.confidence)[0];
2662
+ if (freshestEntry && !selectedEntries.some((entry) => entry.id === freshestEntry.id)) {
2663
+ selectedEntries[selectedEntries.length - 1] = freshestEntry;
2664
+ }
2267
2665
  for (const entry of selectedEntries) {
2268
2666
  allFacts.push({
2269
- entityKey: `${resolvedInfo.entityType}/${resolvedInfo.entityId}/${entry.key}`,
2667
+ entityKey: `${resolvedInfo.entityType}/${resolvedInfo.entityId}/${normalizeContinuityKey(entry.key)}`,
2270
2668
  summary: entry.valueSummary,
2271
2669
  value: entry.valueRaw,
2272
2670
  confidence: entry.confidence,
2273
2671
  source: entry.source,
2672
+ lastUpdated: entry.updatedAt.toISOString(),
2274
2673
  entryId: entry.id,
2275
2674
  });
2276
2675
  }
@@ -2312,13 +2711,15 @@ ${detectionWindow}`,
2312
2711
  });
2313
2712
  (0, metrics_1.timeEnd)('attendant.observe_ms', t0);
2314
2713
  return {
2315
- facts: topFacts.map(({ entityKey, summary, value, confidence, source }) => ({
2714
+ facts: (0, hostMemoryFormatting_1.assignStructuredFactIds)(topFacts.map(({ entityKey, summary, value, confidence, source, lastUpdated, entryId }) => ({
2715
+ knowledgeEntryId: entryId,
2316
2716
  entityKey,
2317
2717
  summary,
2318
2718
  value,
2319
2719
  confidence,
2320
2720
  source,
2321
- })),
2721
+ lastUpdated,
2722
+ }))),
2322
2723
  entitiesDetected: Array.from(entitiesDetected),
2323
2724
  alreadyPresent,
2324
2725
  totalFound: allFacts.length,
@@ -2381,21 +2782,21 @@ ${detectionWindow}`,
2381
2782
  const response = await (0, router_1.route)('classification', [
2382
2783
  {
2383
2784
  role: 'user',
2384
- content: `Decide whether this assistant should fetch persistent memory before replying.
2385
-
2386
- Latest user message:
2387
- ${input.latestMessage || '(none)'}
2388
-
2389
- Recent context excerpt:
2390
- ${contextWindow || '(empty)'}
2391
-
2392
- Return ONLY valid JSON with this exact shape:
2393
- {"needsMemory":true,"confidence":0.81,"reason":"short_reason"}
2394
-
2395
- Rules:
2396
- - needsMemory=true when the message involves project context, technical decisions, code state, prior work, open tasks, bugs, architecture, preferences, or anything session- or project-specific.
2397
- - needsMemory=true when in doubt — false positives are cheap, false negatives lose context.
2398
- - needsMemory=false ONLY for clear one-word acks, simple greetings, or purely generic factual questions with no project relevance.
2785
+ content: `Decide whether this assistant should fetch persistent memory before replying.
2786
+
2787
+ Latest user message:
2788
+ ${input.latestMessage || '(none)'}
2789
+
2790
+ Recent context excerpt:
2791
+ ${contextWindow || '(empty)'}
2792
+
2793
+ Return ONLY valid JSON with this exact shape:
2794
+ {"needsMemory":true,"confidence":0.81,"reason":"short_reason"}
2795
+
2796
+ Rules:
2797
+ - needsMemory=true when the message involves project context, technical decisions, code state, prior work, open tasks, bugs, architecture, preferences, or anything session- or project-specific.
2798
+ - needsMemory=true when in doubt — false positives are cheap, false negatives lose context.
2799
+ - needsMemory=false ONLY for clear one-word acks, simple greetings, or purely generic factual questions with no project relevance.
2399
2800
  - confidence is a float from 0 to 1.`,
2400
2801
  },
2401
2802
  ], 128);
@@ -2758,14 +3159,14 @@ Rules:
2758
3159
  const response = await (0, router_1.route)('task_inference', [
2759
3160
  {
2760
3161
  role: 'user',
2761
- content: `You are analyzing what an AI agent is currently working on.
2762
-
2763
- Agent ID: ${this.agentId}
2764
- Task description: ${context.task}
2765
- Recent messages:
2766
- ${context.recentMessages.map((m, i) => `${i + 1}. ${m}`).join('\n')}
2767
-
2768
- In one short sentence, describe the specific type of task this agent is currently performing.
3162
+ content: `You are analyzing what an AI agent is currently working on.
3163
+
3164
+ Agent ID: ${this.agentId}
3165
+ Task description: ${context.task}
3166
+ Recent messages:
3167
+ ${context.recentMessages.map((m, i) => `${i + 1}. ${m}`).join('\n')}
3168
+
3169
+ In one short sentence, describe the specific type of task this agent is currently performing.
2769
3170
  Be specific and concrete.`,
2770
3171
  },
2771
3172
  ], 256);
@@ -2798,15 +3199,15 @@ Be specific and concrete.`,
2798
3199
  const response = await (0, router_1.route)('relevance_filtering', [
2799
3200
  {
2800
3201
  role: 'user',
2801
- content: `You are deciding what knowledge an AI agent needs for its current task.
2802
-
2803
- Agent task: ${taskType}
2804
-
2805
- Available knowledge entries:
2806
- ${entryInputs.map((e, i) => `${i + 1}. [${e.key}] ${e.valueSummary} (confidence: ${e.confidence})`).join('\n')}
2807
-
2808
- Return only the numbers of entries that are directly relevant to the current task.
2809
- Format: comma-separated numbers only. Example: 1,3,5
3202
+ content: `You are deciding what knowledge an AI agent needs for its current task.
3203
+
3204
+ Agent task: ${taskType}
3205
+
3206
+ Available knowledge entries:
3207
+ ${entryInputs.map((e, i) => `${i + 1}. [${e.key}] ${e.valueSummary} (confidence: ${e.confidence})`).join('\n')}
3208
+
3209
+ Return only the numbers of entries that are directly relevant to the current task.
3210
+ Format: comma-separated numbers only. Example: 1,3,5
2810
3211
  If nothing is relevant, return: none`,
2811
3212
  },
2812
3213
  ], 128);
@@ -2832,6 +3233,7 @@ If nothing is relevant, return: none`,
2832
3233
  this.brief = {
2833
3234
  ...this.brief,
2834
3235
  compliance: this.buildComplianceState(),
3236
+ pendingMemoryAttributions: this.pendingMemoryAttributions.map((entry) => ({ ...entry })),
2835
3237
  };
2836
3238
  await (0, client_1.getDb)().knowledgeEntry.upsert({
2837
3239
  where: {
@@ -2871,8 +3273,12 @@ If nothing is relevant, return: none`,
2871
3273
  this.sharedStateObservedAt = state.briefGeneratedAt;
2872
3274
  this.attendsWithoutPersist = state.compliance?.counters.attendsWithoutPersist ?? 0;
2873
3275
  this.consecutivePreResponseWithoutPost = state.compliance?.counters.consecutivePreResponseWithoutPost ?? 0;
3276
+ this.consecutiveUnusedMemoryInjections = state.compliance?.counters.consecutiveUnusedMemoryInjections ?? 0;
2874
3277
  this.lastAttendPhase = state.compliance?.counters.lastAttendPhase ?? undefined;
2875
3278
  this.complianceUpdatedAt = state.compliance?.lastUpdated ?? state.briefGeneratedAt;
3279
+ this.pendingMemoryAttributions = Array.isArray(state.pendingMemoryAttributions)
3280
+ ? state.pendingMemoryAttributions.map((entry) => ({ ...entry }))
3281
+ : [];
2876
3282
  this.brief = state;
2877
3283
  return state;
2878
3284
  }