iranti 0.3.0 → 0.3.3

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 (64) hide show
  1. package/README.md +48 -44
  2. package/dist/scripts/claude-code-memory-hook.js +42 -153
  3. package/dist/scripts/codex-setup.js +1 -1
  4. package/dist/scripts/iranti-cli.js +161 -17
  5. package/dist/scripts/iranti-mcp.js +80 -8
  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/attendant/AttendantInstance.d.ts +44 -1
  18. package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
  19. package/dist/src/attendant/AttendantInstance.js +475 -41
  20. package/dist/src/attendant/AttendantInstance.js.map +1 -1
  21. package/dist/src/attendant/index.d.ts +1 -1
  22. package/dist/src/attendant/index.d.ts.map +1 -1
  23. package/dist/src/attendant/index.js.map +1 -1
  24. package/dist/src/chat/index.d.ts +2 -0
  25. package/dist/src/chat/index.d.ts.map +1 -1
  26. package/dist/src/chat/index.js +56 -22
  27. package/dist/src/chat/index.js.map +1 -1
  28. package/dist/src/lib/assistantCheckpoint.d.ts +21 -0
  29. package/dist/src/lib/assistantCheckpoint.d.ts.map +1 -0
  30. package/dist/src/lib/assistantCheckpoint.js +143 -0
  31. package/dist/src/lib/assistantCheckpoint.js.map +1 -0
  32. package/dist/src/lib/cliHelpCatalog.d.ts.map +1 -1
  33. package/dist/src/lib/cliHelpCatalog.js +6 -5
  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 +56 -0
  38. package/dist/src/lib/hostMemoryFormatting.js.map +1 -0
  39. package/dist/src/lib/llm.d.ts.map +1 -1
  40. package/dist/src/lib/llm.js +3 -1
  41. package/dist/src/lib/llm.js.map +1 -1
  42. package/dist/src/lib/projectLearning.d.ts +21 -0
  43. package/dist/src/lib/projectLearning.d.ts.map +1 -0
  44. package/dist/src/lib/projectLearning.js +357 -0
  45. package/dist/src/lib/projectLearning.js.map +1 -0
  46. package/dist/src/lib/protocolEnforcement.d.ts +3 -1
  47. package/dist/src/lib/protocolEnforcement.d.ts.map +1 -1
  48. package/dist/src/lib/protocolEnforcement.js +28 -2
  49. package/dist/src/lib/protocolEnforcement.js.map +1 -1
  50. package/dist/src/lib/sessionLedger.d.ts +18 -0
  51. package/dist/src/lib/sessionLedger.d.ts.map +1 -1
  52. package/dist/src/lib/sessionLedger.js +78 -0
  53. package/dist/src/lib/sessionLedger.js.map +1 -1
  54. package/dist/src/librarian/index.d.ts.map +1 -1
  55. package/dist/src/librarian/index.js +51 -0
  56. package/dist/src/librarian/index.js.map +1 -1
  57. package/dist/src/library/client.d.ts.map +1 -1
  58. package/dist/src/library/client.js +0 -1
  59. package/dist/src/library/client.js.map +1 -1
  60. package/dist/src/sdk/index.d.ts +2 -0
  61. package/dist/src/sdk/index.d.ts.map +1 -1
  62. package/dist/src/sdk/index.js +39 -2
  63. package/dist/src/sdk/index.js.map +1 -1
  64. 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.",
@@ -65,8 +72,22 @@ const ATTEND_EXPECTED_CALL_SEQUENCE = [
65
72
  "Call iranti_attend(phase='post-response') after every response without exception — even short replies may contain durable findings. Omitting this call is a compliance violation.",
66
73
  'Call iranti_attend again when the new knowledge should change what is loaded next.',
67
74
  ];
68
- 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.';
75
+ const ATTEND_USAGE_REMINDER = 'Iranti is a hive mind. MANDATORY: call iranti_attend before every reply and around knowledge discovery. MANDATORY: call iranti_write after every file edit, confirmed finding, environment state change, and subagent completion write what changed, why, and what it means. Skipping writes means the next session starts blind and must rediscover everything from scratch.';
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',
@@ -254,10 +352,20 @@ function advisoryTaskTokens(taskType) {
254
352
  .map((token) => token.trim())
255
353
  .filter((token) => token.length >= 4)));
256
354
  }
257
- function buildUsageGuidance(tool) {
355
+ function buildUsageGuidance(tool, turnsWithoutWrite = 0) {
356
+ let reminder = ATTEND_USAGE_REMINDER;
357
+ if (turnsWithoutWrite >= 3) {
358
+ reminder += ` NON-COMPLIANT: ${turnsWithoutWrite} turns have completed without a single iranti_write call. You are losing knowledge. Call iranti_write NOW for any findings, file changes, or decisions from recent turns.`;
359
+ }
360
+ else if (turnsWithoutWrite >= 2) {
361
+ reminder += ` WARNING: ${turnsWithoutWrite} turns without an iranti_write call. If you discovered, changed, or confirmed anything, write it now before it is lost.`;
362
+ }
363
+ else if (turnsWithoutWrite === 1) {
364
+ reminder += ' Reminder: if the previous turn produced durable findings, call iranti_write before continuing.';
365
+ }
258
366
  return {
259
367
  tool,
260
- reminder: ATTEND_USAGE_REMINDER,
368
+ reminder,
261
369
  expectedCallSequence: ATTEND_EXPECTED_CALL_SEQUENCE,
262
370
  note: tool === 'observe'
263
371
  ? OBSERVE_USAGE_NOTE
@@ -489,6 +597,28 @@ function buildSessionComplianceState(input) {
489
597
  requiredAction: 'Persist durable findings with iranti_write or iranti_checkpoint before the next turn if new knowledge, validation, or file changes occurred.',
490
598
  });
491
599
  }
600
+ if (input.turnsWithoutWrite >= 2) {
601
+ const severity = input.turnsWithoutWrite >= 3 ? 'error' : 'warn';
602
+ issues.push({
603
+ code: 'missing_writes_across_turns',
604
+ severity,
605
+ count: input.turnsWithoutWrite,
606
+ message: `${input.turnsWithoutWrite} completed turns without a single iranti_write or iranti_checkpoint call. Knowledge discovered during these turns is not being persisted.`,
607
+ requiredAction: 'Call iranti_write for each durable finding — file edits, confirmed facts, environment state, subagent results. Every turn that discovers something should write it.',
608
+ });
609
+ }
610
+ if (input.consecutiveUnusedMemoryInjections > 0) {
611
+ const severity = input.consecutiveUnusedMemoryInjections >= 2 ? 'error' : 'warn';
612
+ issues.push({
613
+ code: 'ignored_injected_memory',
614
+ severity,
615
+ count: input.consecutiveUnusedMemoryInjections,
616
+ message: input.consecutiveUnusedMemoryInjections >= 2
617
+ ? `Injected memory has been surfaced and then ignored across ${input.consecutiveUnusedMemoryInjections} consecutive turns.`
618
+ : 'Injected memory was surfaced but the response did not use it in the previous turn.',
619
+ 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.',
620
+ });
621
+ }
492
622
  let status = 'healthy';
493
623
  if (issues.some((issue) => issue.severity === 'error')) {
494
624
  status = 'non_compliant';
@@ -501,10 +631,14 @@ function buildSessionComplianceState(input) {
501
631
  ? 'Lifecycle is currently in progress and waiting for a post-response attend.'
502
632
  : 'Lifecycle is currently compliant.'
503
633
  : status === 'degraded'
504
- ? 'Lifecycle is degraded: persistence breadcrumbs are lagging.'
634
+ ? input.consecutiveUnusedMemoryInjections > 0
635
+ ? 'Lifecycle is degraded: injected memory was surfaced but not used.'
636
+ : 'Lifecycle is degraded: iranti_write has not been called after recent knowledge-changing actions.'
505
637
  : input.consecutivePreResponseWithoutPost > 0
506
638
  ? 'Lifecycle is non-compliant: the previous turn is still missing a post-response attend.'
507
- : 'Lifecycle is non-compliant: accountability breadcrumbs are missing.';
639
+ : input.consecutiveUnusedMemoryInjections > 0
640
+ ? 'Lifecycle is non-compliant: injected memory is being ignored instead of used or explicitly challenged.'
641
+ : 'Lifecycle is non-compliant: iranti_write calls are missing — durable findings are not being persisted.';
508
642
  return {
509
643
  status,
510
644
  summary,
@@ -512,7 +646,9 @@ function buildSessionComplianceState(input) {
512
646
  lastUpdated: input.lastUpdated ?? new Date().toISOString(),
513
647
  counters: {
514
648
  attendsWithoutPersist: input.attendsWithoutPersist,
649
+ turnsWithoutWrite: input.turnsWithoutWrite,
515
650
  consecutivePreResponseWithoutPost: input.consecutivePreResponseWithoutPost,
651
+ consecutiveUnusedMemoryInjections: input.consecutiveUnusedMemoryInjections,
516
652
  pendingPostResponse,
517
653
  lastAttendPhase: input.lastAttendPhase ?? null,
518
654
  },
@@ -970,13 +1106,13 @@ async function persistSharedCheckpointBreadcrumbs(params) {
970
1106
  if (checkpoint.currentStep) {
971
1107
  await (0, librarian_1.librarianWrite)({
972
1108
  ...common,
973
- key: 'checkpoint_current_step',
1109
+ key: 'current_step',
974
1110
  valueRaw: { text: checkpoint.currentStep },
975
- valueSummary: truncate(`checkpoint current step is ${checkpoint.currentStep}`, 220),
1111
+ valueSummary: truncate(`current step is ${checkpoint.currentStep}`, 220),
976
1112
  properties: {
977
1113
  ...checkpointBaseProperties,
978
1114
  durableClass: 'current_step',
979
- canonicalKey: 'checkpoint_current_step',
1115
+ canonicalKey: 'current_step',
980
1116
  mergeStrategy: 'replace',
981
1117
  ...(0, semanticFactTags_1.buildSemanticFactTags)({
982
1118
  memoryScope: 'project',
@@ -989,19 +1125,32 @@ async function persistSharedCheckpointBreadcrumbs(params) {
989
1125
  expectedKeys.push({
990
1126
  entityType: resolved.entityType,
991
1127
  entityId: resolved.entityId,
992
- key: 'checkpoint_current_step',
1128
+ key: 'current_step',
993
1129
  });
994
1130
  }
995
1131
  if (checkpoint.nextStep) {
1132
+ const existingNextStep = await (0, queries_2.findEntry)({
1133
+ entityType: resolved.entityType,
1134
+ entityId: resolved.entityId,
1135
+ key: 'next_step',
1136
+ });
1137
+ const priorInstruction = existingNextStep?.valueRaw && typeof existingNextStep.valueRaw === 'object'
1138
+ ? existingNextStep.valueRaw.instruction
1139
+ : null;
1140
+ const mergedNextStep = typeof priorInstruction === 'string'
1141
+ && priorInstruction.trim().length > 0
1142
+ && priorInstruction.trim() !== checkpoint.nextStep.trim()
1143
+ ? `${checkpoint.nextStep}. Prior task step: ${priorInstruction.trim()}`
1144
+ : checkpoint.nextStep;
996
1145
  await (0, librarian_1.librarianWrite)({
997
1146
  ...common,
998
- key: 'checkpoint_next_step',
999
- valueRaw: { instruction: checkpoint.nextStep },
1000
- valueSummary: truncate(`checkpoint next step is ${checkpoint.nextStep}`, 220),
1147
+ key: 'next_step',
1148
+ valueRaw: { instruction: mergedNextStep },
1149
+ valueSummary: truncate(`next step is ${mergedNextStep}`, 220),
1001
1150
  properties: {
1002
1151
  ...checkpointBaseProperties,
1003
1152
  durableClass: 'next_step',
1004
- canonicalKey: 'checkpoint_next_step',
1153
+ canonicalKey: 'next_step',
1005
1154
  mergeStrategy: 'replace',
1006
1155
  ...(0, semanticFactTags_1.buildSemanticFactTags)({
1007
1156
  memoryScope: 'project',
@@ -1014,7 +1163,7 @@ async function persistSharedCheckpointBreadcrumbs(params) {
1014
1163
  expectedKeys.push({
1015
1164
  entityType: resolved.entityType,
1016
1165
  entityId: resolved.entityId,
1017
- key: 'checkpoint_next_step',
1166
+ key: 'next_step',
1018
1167
  });
1019
1168
  }
1020
1169
  if (Array.isArray(checkpoint.fileChanges) && checkpoint.fileChanges.length > 0) {
@@ -1082,13 +1231,13 @@ async function persistSharedCheckpointBreadcrumbs(params) {
1082
1231
  if (checkpoint.openRisks && checkpoint.openRisks.length > 0) {
1083
1232
  await (0, librarian_1.librarianWrite)({
1084
1233
  ...common,
1085
- key: 'checkpoint_open_risks',
1234
+ key: 'open_risks',
1086
1235
  valueRaw: { items: checkpoint.openRisks },
1087
- valueSummary: truncate(`checkpoint open risks include ${checkpoint.openRisks.join('; ')}`, 220),
1236
+ valueSummary: truncate(`open risks include ${checkpoint.openRisks.join('; ')}`, 220),
1088
1237
  properties: {
1089
1238
  ...checkpointBaseProperties,
1090
1239
  durableClass: 'open_risks',
1091
- canonicalKey: 'checkpoint_open_risks',
1240
+ canonicalKey: 'open_risks',
1092
1241
  mergeStrategy: 'replace',
1093
1242
  ...(0, semanticFactTags_1.buildSemanticFactTags)({
1094
1243
  memoryScope: 'project',
@@ -1101,7 +1250,7 @@ async function persistSharedCheckpointBreadcrumbs(params) {
1101
1250
  expectedKeys.push({
1102
1251
  entityType: resolved.entityType,
1103
1252
  entityId: resolved.entityId,
1104
- key: 'checkpoint_open_risks',
1253
+ key: 'open_risks',
1105
1254
  });
1106
1255
  }
1107
1256
  }
@@ -1199,7 +1348,9 @@ class AttendantInstance {
1199
1348
  this.advisoryLearningProfile = null;
1200
1349
  this.contextCallCount = 0;
1201
1350
  this.attendsWithoutPersist = 0;
1351
+ this.turnsWithoutWrite = 0;
1202
1352
  this.consecutivePreResponseWithoutPost = 0;
1353
+ this.consecutiveUnusedMemoryInjections = 0;
1203
1354
  this.lastAttendPhase = undefined;
1204
1355
  this.complianceUpdatedAt = new Date().toISOString();
1205
1356
  this.sessionStarted = new Date().toISOString();
@@ -1208,6 +1359,7 @@ class AttendantInstance {
1208
1359
  this.eventHost = null;
1209
1360
  this.sharedStateObservedAt = null;
1210
1361
  this.pendingSharedStateInvalidations = new Map();
1362
+ this.pendingMemoryAttributions = [];
1211
1363
  this.agentId = agentId;
1212
1364
  (0, sharedStateInvalidation_1.registerSharedStateInvalidationObserver)(agentId, this);
1213
1365
  }
@@ -1232,6 +1384,156 @@ class AttendantInstance {
1232
1384
  ...(this.eventHost ? { host: this.eventHost } : {}),
1233
1385
  };
1234
1386
  }
1387
+ updateBriefPendingMemoryAttributions() {
1388
+ if (!this.brief)
1389
+ return;
1390
+ this.brief = {
1391
+ ...this.brief,
1392
+ pendingMemoryAttributions: this.pendingMemoryAttributions.map((entry) => ({ ...entry })),
1393
+ };
1394
+ }
1395
+ addPendingMemoryAttribution(input) {
1396
+ const attribution = {
1397
+ injectionId: (0, crypto_1.randomUUID)(),
1398
+ surfaced: true,
1399
+ used: false,
1400
+ helpful: false,
1401
+ status: 'pending',
1402
+ phase: input.phase,
1403
+ surfacedAt: new Date().toISOString(),
1404
+ reason: 'awaiting_post_response_evaluation',
1405
+ injectedKeys: [...input.injectedKeys],
1406
+ injectedEntryIds: [...input.injectedEntryIds],
1407
+ evidenceKinds: [],
1408
+ };
1409
+ this.pendingMemoryAttributions.push(attribution);
1410
+ this.updateBriefPendingMemoryAttributions();
1411
+ return attribution;
1412
+ }
1413
+ recordMemoryEvidence(kind) {
1414
+ if (this.pendingMemoryAttributions.length === 0) {
1415
+ return;
1416
+ }
1417
+ for (const attribution of this.pendingMemoryAttributions) {
1418
+ if (attribution.status !== 'pending')
1419
+ continue;
1420
+ if (!attribution.evidenceKinds.includes(kind)) {
1421
+ attribution.evidenceKinds = [...attribution.evidenceKinds, kind];
1422
+ }
1423
+ (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
1424
+ staffComponent: 'Attendant',
1425
+ actionType: 'memory_evidence_observed',
1426
+ agentId: this.agentId,
1427
+ source: this.eventSource,
1428
+ reason: kind,
1429
+ level: 'audit',
1430
+ metadata: this.buildEventMetadata({
1431
+ injectionId: attribution.injectionId,
1432
+ injectedKeys: attribution.injectedKeys,
1433
+ injectedEntryIds: attribution.injectedEntryIds,
1434
+ evidenceKind: kind,
1435
+ }),
1436
+ });
1437
+ }
1438
+ this.updateBriefPendingMemoryAttributions();
1439
+ }
1440
+ responseMentionsInjectedMemory(response, attribution) {
1441
+ const responseTokens = new Set(tokenize(response));
1442
+ if (responseTokens.size === 0)
1443
+ return false;
1444
+ for (const entityKey of attribution.injectedKeys) {
1445
+ const key = entityKey.split('/').slice(2).join('/');
1446
+ for (const token of tokenize(key.replace(/[_/.-]+/g, ' '))) {
1447
+ if (responseTokens.has(token)) {
1448
+ return true;
1449
+ }
1450
+ }
1451
+ }
1452
+ return false;
1453
+ }
1454
+ responseShowsRecoveryValue(response, attribution) {
1455
+ const normalized = normalizeText(response);
1456
+ if (!normalized)
1457
+ return false;
1458
+ return attribution.injectedKeys.some((entityKey) => {
1459
+ const key = entityKey.split('/').slice(2).join('/');
1460
+ return (/\b(next step|current step|blocker|blockers|risk|risks|status|progress|file|files|changed|handoff|resume|recovery)\b/.test(normalized)
1461
+ && /\b(next_step|current_step|open_risks|status|checkpoint_summary|recent_file_changes|recent_actions|implementation_status|blockers?)\b/i.test(key));
1462
+ });
1463
+ }
1464
+ scorePendingMemoryAttributions(response) {
1465
+ if (this.pendingMemoryAttributions.length === 0) {
1466
+ return [];
1467
+ }
1468
+ const scoredAt = new Date().toISOString();
1469
+ const scored = this.pendingMemoryAttributions.map((entry) => {
1470
+ const evidenceKinds = [...entry.evidenceKinds];
1471
+ const rediscoveredManually = evidenceKinds.includes('rediscovery');
1472
+ if (!rediscoveredManually && this.responseMentionsInjectedMemory(response, entry) && !evidenceKinds.includes('response_reference')) {
1473
+ evidenceKinds.push('response_reference');
1474
+ }
1475
+ if (!rediscoveredManually && this.responseShowsRecoveryValue(response, entry) && !evidenceKinds.includes('response_recovery')) {
1476
+ evidenceKinds.push('response_recovery');
1477
+ }
1478
+ const used = evidenceKinds.includes('write')
1479
+ || evidenceKinds.includes('checkpoint')
1480
+ || evidenceKinds.includes('response_reference')
1481
+ || evidenceKinds.includes('response_recovery');
1482
+ const helpful = evidenceKinds.includes('checkpoint')
1483
+ || evidenceKinds.includes('write')
1484
+ || evidenceKinds.includes('response_recovery');
1485
+ const reason = helpful
1486
+ ? 'response_or_action_confirmed_memory_helpfulness'
1487
+ : used
1488
+ ? 'response_referenced_injected_memory'
1489
+ : 'memory_was_only_surfaced';
1490
+ const scoredEntry = {
1491
+ ...entry,
1492
+ used,
1493
+ helpful,
1494
+ status: 'scored',
1495
+ scoredAt,
1496
+ reason,
1497
+ evidenceKinds,
1498
+ };
1499
+ (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
1500
+ staffComponent: 'Attendant',
1501
+ actionType: 'memory_injection_scored',
1502
+ agentId: this.agentId,
1503
+ source: this.eventSource,
1504
+ reason,
1505
+ level: 'audit',
1506
+ metadata: this.buildEventMetadata({
1507
+ injectionId: scoredEntry.injectionId,
1508
+ surfaced: true,
1509
+ used,
1510
+ helpful,
1511
+ phase: scoredEntry.phase,
1512
+ injectedKeys: scoredEntry.injectedKeys,
1513
+ injectedEntryIds: scoredEntry.injectedEntryIds,
1514
+ evidenceKinds,
1515
+ scoredAt,
1516
+ }),
1517
+ });
1518
+ return scoredEntry;
1519
+ });
1520
+ if (scored.some((entry) => entry.used)) {
1521
+ this.consecutiveUnusedMemoryInjections = 0;
1522
+ }
1523
+ else if (scored.some((entry) => entry.surfaced)) {
1524
+ this.consecutiveUnusedMemoryInjections += 1;
1525
+ }
1526
+ this.pendingMemoryAttributions = [];
1527
+ this.updateBriefPendingMemoryAttributions();
1528
+ return scored;
1529
+ }
1530
+ async noteDiscoveryOccurred() {
1531
+ if (this.pendingMemoryAttributions.length === 0) {
1532
+ return;
1533
+ }
1534
+ this.recordMemoryEvidence('rediscovery');
1535
+ await this.persistState();
1536
+ }
1235
1537
  async loadSessionLedgerSignals(taskType) {
1236
1538
  try {
1237
1539
  const source = this.eventSource === 'internal' ? undefined : this.eventSource;
@@ -1260,6 +1562,33 @@ class AttendantInstance {
1260
1562
  return { learnings: [], profile: null };
1261
1563
  }
1262
1564
  }
1565
+ async loadProjectPolicies() {
1566
+ const configured = (0, autoRemember_1.getProjectMemoryEntity)();
1567
+ if (!configured)
1568
+ return [];
1569
+ const parsed = (0, entity_resolution_1.parseEntityString)(configured);
1570
+ const entries = await (0, queries_1.findEntriesByEntity)(parsed.entityType, parsed.entityId);
1571
+ const policies = entries
1572
+ .filter((entry) => isProjectPolicyEntry({
1573
+ key: entry.key,
1574
+ properties: entry.properties ?? null,
1575
+ }))
1576
+ .map((entry) => {
1577
+ const rules = normalizeProjectPolicyRuleLines(entry.valueRaw, entry.valueSummary);
1578
+ if (rules.length === 0)
1579
+ return null;
1580
+ return {
1581
+ entityKey: `${entry.entityType}/${entry.entityId}/${entry.key}`,
1582
+ summary: rules.join(' '),
1583
+ key: entry.key,
1584
+ source: entry.source,
1585
+ lastUpdated: entry.updatedAt.toISOString(),
1586
+ rules,
1587
+ };
1588
+ })
1589
+ .filter((entry) => Boolean(entry));
1590
+ return policies;
1591
+ }
1263
1592
  // ── Handshake ────────────────────────────────────────────────────────────
1264
1593
  async handshake(context) {
1265
1594
  const t0 = (0, metrics_1.timeStart)();
@@ -1271,13 +1600,15 @@ class AttendantInstance {
1271
1600
  // Infer task type
1272
1601
  const inferredTaskType = await this.inferTask(context);
1273
1602
  // Load knowledge — agent entries + related entities
1274
- const [workingMemory, ledgerSignals] = await Promise.all([
1603
+ const [workingMemory, ledgerSignals, projectPolicies] = await Promise.all([
1275
1604
  this.buildWorkingMemory(inferredTaskType),
1276
1605
  this.loadSessionLedgerSignals(inferredTaskType),
1606
+ this.loadProjectPolicies(),
1277
1607
  ]);
1278
1608
  const sessionLedgerLearnings = ledgerSignals.learnings;
1279
1609
  this.advisoryLearningProfile = ledgerSignals.profile;
1280
- const workingMemoryWithLedger = mergeWorkingMemoryWithLedger(workingMemory, sessionLedgerLearnings);
1610
+ const workingMemoryWithPolicies = mergeWorkingMemoryWithProjectPolicies(workingMemory, projectPolicies);
1611
+ const workingMemoryWithLedger = mergeWorkingMemoryWithLedger(workingMemoryWithPolicies, sessionLedgerLearnings);
1281
1612
  const recoveryResult = persisted?.sessionCheckpoint
1282
1613
  ? this.buildRecovery(context, persisted.sessionCheckpoint)
1283
1614
  : { interrupted: false, recovery: null };
@@ -1294,9 +1625,10 @@ class AttendantInstance {
1294
1625
  }
1295
1626
  this.brief = {
1296
1627
  agentId: this.agentId,
1297
- operatingRules: applyAdvisoryOperatingRules(operatingRules, this.advisoryLearningProfile),
1628
+ operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(operatingRules, projectPolicies), this.advisoryLearningProfile),
1298
1629
  inferredTaskType,
1299
1630
  workingMemory: workingMemoryWithLedger,
1631
+ projectPolicies,
1300
1632
  sessionStarted: persisted?.sessionStarted ?? this.sessionStarted,
1301
1633
  briefGeneratedAt: new Date().toISOString(),
1302
1634
  contextCallCount: this.contextCallCount,
@@ -1323,6 +1655,7 @@ class AttendantInstance {
1323
1655
  metadata: this.buildEventMetadata({
1324
1656
  briefSize: this.brief?.workingMemory.length ?? 0,
1325
1657
  ledgerLearningCount: sessionLedgerLearnings.length,
1658
+ projectPolicyCount: projectPolicies.length,
1326
1659
  advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
1327
1660
  taskSummary: context.task.slice(0, 120),
1328
1661
  }),
@@ -1340,7 +1673,10 @@ class AttendantInstance {
1340
1673
  return result;
1341
1674
  }
1342
1675
  const newTaskType = await this.inferTask(context);
1343
- const ledgerSignals = await this.loadSessionLedgerSignals(newTaskType);
1676
+ const [ledgerSignals, projectPolicies] = await Promise.all([
1677
+ this.loadSessionLedgerSignals(newTaskType),
1678
+ this.loadProjectPolicies(),
1679
+ ]);
1344
1680
  this.advisoryLearningProfile = ledgerSignals.profile;
1345
1681
  // Task hasn't shifted — update timestamp only
1346
1682
  if (newTaskType.toLowerCase() === this.brief.inferredTaskType.toLowerCase()) {
@@ -1354,8 +1690,9 @@ class AttendantInstance {
1354
1690
  }
1355
1691
  this.brief = {
1356
1692
  ...this.brief,
1357
- operatingRules: applyAdvisoryOperatingRules(this.brief.operatingRules, this.advisoryLearningProfile),
1358
- workingMemory: mergeWorkingMemoryWithLedger(this.brief.workingMemory, ledgerSignals.learnings),
1693
+ operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(await this.loadOperatingRules(), projectPolicies), this.advisoryLearningProfile),
1694
+ workingMemory: mergeWorkingMemoryWithLedger(mergeWorkingMemoryWithProjectPolicies(this.brief.workingMemory, projectPolicies), ledgerSignals.learnings),
1695
+ projectPolicies,
1359
1696
  briefGeneratedAt: new Date().toISOString(),
1360
1697
  contextCallCount: this.contextCallCount,
1361
1698
  sessionLedgerLearnings: ledgerSignals.learnings,
@@ -1375,6 +1712,7 @@ class AttendantInstance {
1375
1712
  metadata: this.buildEventMetadata({
1376
1713
  briefSize: this.brief?.workingMemory.length ?? 0,
1377
1714
  contextCallCount: this.contextCallCount,
1715
+ projectPolicyCount: projectPolicies.length,
1378
1716
  advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
1379
1717
  }),
1380
1718
  });
@@ -1385,9 +1723,10 @@ class AttendantInstance {
1385
1723
  const workingMemory = await this.buildWorkingMemory(newTaskType);
1386
1724
  this.brief = {
1387
1725
  ...this.brief,
1388
- operatingRules: applyAdvisoryOperatingRules(this.brief.operatingRules, this.advisoryLearningProfile),
1726
+ operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(await this.loadOperatingRules(), projectPolicies), this.advisoryLearningProfile),
1389
1727
  inferredTaskType: newTaskType,
1390
- workingMemory: mergeWorkingMemoryWithLedger(workingMemory, ledgerSignals.learnings),
1728
+ workingMemory: mergeWorkingMemoryWithLedger(mergeWorkingMemoryWithProjectPolicies(workingMemory, projectPolicies), ledgerSignals.learnings),
1729
+ projectPolicies,
1391
1730
  briefGeneratedAt: new Date().toISOString(),
1392
1731
  contextCallCount: this.contextCallCount,
1393
1732
  sessionLedgerLearnings: ledgerSignals.learnings,
@@ -1407,6 +1746,7 @@ class AttendantInstance {
1407
1746
  metadata: this.buildEventMetadata({
1408
1747
  briefSize: this.brief?.workingMemory.length ?? 0,
1409
1748
  contextCallCount: this.contextCallCount,
1749
+ projectPolicyCount: projectPolicies.length,
1410
1750
  advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
1411
1751
  }),
1412
1752
  });
@@ -1434,8 +1774,10 @@ class AttendantInstance {
1434
1774
  const operatingRules = rulesResult.found && rulesResult.entry
1435
1775
  ? formatOperatingRulesText(rulesResult.entry.valueRaw, rulesResult.entry.valueSummary)
1436
1776
  : formatOperatingRulesText(null, 'Attendant operating rules:');
1777
+ const projectPolicies = this.brief?.projectPolicies ?? await this.loadProjectPolicies();
1437
1778
  if (this.brief) {
1438
- this.brief.operatingRules = operatingRules;
1779
+ this.brief.operatingRules = applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(operatingRules, projectPolicies), this.advisoryLearningProfile);
1780
+ this.brief.projectPolicies = projectPolicies;
1439
1781
  this.brief.contextCallCount = 0;
1440
1782
  }
1441
1783
  this.contextCallCount = 0;
@@ -1472,16 +1814,20 @@ class AttendantInstance {
1472
1814
  buildComplianceState(lastUpdated) {
1473
1815
  return buildSessionComplianceState({
1474
1816
  attendsWithoutPersist: this.attendsWithoutPersist,
1817
+ turnsWithoutWrite: this.turnsWithoutWrite,
1475
1818
  consecutivePreResponseWithoutPost: this.consecutivePreResponseWithoutPost,
1819
+ consecutiveUnusedMemoryInjections: this.consecutiveUnusedMemoryInjections,
1476
1820
  lastAttendPhase: this.lastAttendPhase,
1477
1821
  lastUpdated: lastUpdated ?? this.complianceUpdatedAt,
1478
1822
  });
1479
1823
  }
1480
1824
  async notifyWriteOccurred() {
1481
1825
  this.attendsWithoutPersist = 0;
1826
+ this.turnsWithoutWrite = 0;
1482
1827
  this.lastAttendPhase = undefined;
1483
1828
  this.consecutivePreResponseWithoutPost = 0;
1484
1829
  this.complianceUpdatedAt = new Date().toISOString();
1830
+ this.recordMemoryEvidence('write');
1485
1831
  if (!this.brief) {
1486
1832
  return;
1487
1833
  }
@@ -1494,9 +1840,11 @@ class AttendantInstance {
1494
1840
  }
1495
1841
  async checkpoint(input) {
1496
1842
  this.attendsWithoutPersist = 0;
1843
+ this.turnsWithoutWrite = 0;
1497
1844
  this.lastAttendPhase = undefined;
1498
1845
  this.consecutivePreResponseWithoutPost = 0;
1499
1846
  this.complianceUpdatedAt = new Date().toISOString();
1847
+ this.recordMemoryEvidence('checkpoint');
1500
1848
  this.setLedgerContext(input.ledgerContext);
1501
1849
  const now = new Date().toISOString();
1502
1850
  if (!this.brief) {
@@ -1730,9 +2078,12 @@ class AttendantInstance {
1730
2078
  this.attendsWithoutPersist++;
1731
2079
  const phase = input.phase;
1732
2080
  let complianceWarning;
2081
+ 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
2082
  if (phase === 'post-response') {
1734
- // Correct post-response call — reset counters
2083
+ // Correct post-response call — reset attend counters but NOT turnsWithoutWrite
2084
+ // turnsWithoutWrite only resets on actual writes/checkpoints
1735
2085
  this.attendsWithoutPersist = 0;
2086
+ this.turnsWithoutWrite++;
1736
2087
  this.lastAttendPhase = 'post-response';
1737
2088
  this.consecutivePreResponseWithoutPost = 0;
1738
2089
  }
@@ -1753,7 +2104,10 @@ class AttendantInstance {
1753
2104
  }
1754
2105
  }
1755
2106
  this.complianceUpdatedAt = new Date().toISOString();
1756
- const compliance = this.buildComplianceState(this.complianceUpdatedAt);
2107
+ let compliance = this.buildComplianceState(this.complianceUpdatedAt);
2108
+ if (!complianceWarning && compliance.issues.some((issue) => issue.code === 'ignored_injected_memory')) {
2109
+ complianceWarning = ignoredMemoryWarning;
2110
+ }
1757
2111
  if (!this.brief) {
1758
2112
  const bootstrapTask = buildAttendBootstrapTask(latestMessage, currentContext);
1759
2113
  const bootstrapBrief = await this.handshake({
@@ -1788,6 +2142,52 @@ class AttendantInstance {
1788
2142
  }),
1789
2143
  });
1790
2144
  }
2145
+ if (phase === 'post-response') {
2146
+ const memoryAttributions = this.scorePendingMemoryAttributions(latestMessage || currentContext);
2147
+ compliance = this.buildComplianceState(this.complianceUpdatedAt);
2148
+ if (memoryAttributions.some((entry) => !entry.used)) {
2149
+ complianceWarning = ignoredMemoryWarning;
2150
+ }
2151
+ if (this.brief) {
2152
+ this.brief = {
2153
+ ...this.brief,
2154
+ compliance,
2155
+ briefGeneratedAt: this.complianceUpdatedAt,
2156
+ };
2157
+ await this.persistState();
2158
+ }
2159
+ (0, metrics_1.timeEnd)('attendant.attend_ms', t0);
2160
+ return {
2161
+ shouldInject: false,
2162
+ reason: 'memory_not_needed',
2163
+ decision: {
2164
+ needed: false,
2165
+ confidence: 1,
2166
+ method: 'heuristic',
2167
+ explanation: 'post_response_closeout',
2168
+ },
2169
+ bootstrap,
2170
+ complianceWarning,
2171
+ compliance,
2172
+ memoryAttributions,
2173
+ usageGuidance: buildUsageGuidance('attend', this.turnsWithoutWrite),
2174
+ facts: [],
2175
+ entitiesDetected: [],
2176
+ alreadyPresent: 0,
2177
+ totalFound: 0,
2178
+ entitiesResolved: [],
2179
+ debug: {
2180
+ skipped: 'empty_context',
2181
+ contextLength: currentContext.length,
2182
+ detectionWindowChars: Math.min(currentContext.length, ENTITY_DETECTION_WINDOW_CHARS),
2183
+ detectedCandidates: 0,
2184
+ keptCandidates: 0,
2185
+ hintsProvided: effectiveEntityHints.length,
2186
+ hintsResolved: 0,
2187
+ dropped: [{ name: latestMessage || '(none)', reason: 'post_response_closeout' }],
2188
+ },
2189
+ };
2190
+ }
1791
2191
  let decision = await this.decideMemoryNeed({
1792
2192
  currentContext,
1793
2193
  latestMessage,
@@ -1847,7 +2247,8 @@ class AttendantInstance {
1847
2247
  bootstrap,
1848
2248
  complianceWarning,
1849
2249
  compliance,
1850
- usageGuidance: buildUsageGuidance('attend'),
2250
+ memoryAttributions: [],
2251
+ usageGuidance: buildUsageGuidance('attend', this.turnsWithoutWrite),
1851
2252
  facts: [],
1852
2253
  entitiesDetected: [],
1853
2254
  alreadyPresent: 0,
@@ -1870,11 +2271,11 @@ class AttendantInstance {
1870
2271
  currentContext: observationContext,
1871
2272
  maxFacts: input.maxFacts,
1872
2273
  entityHints: observeEntityHints,
1873
- priorityKeys: Array.from(new Set([
2274
+ priorityKeys: expandContinuityPriorityKeys(Array.from(new Set([
1874
2275
  ...(mandatoryRecall.key ? [mandatoryRecall.key] : []),
1875
2276
  ...(this.advisoryLearningProfile?.priorityKeys ?? []),
1876
2277
  ...freshState.priorityKeys,
1877
- ])),
2278
+ ]))),
1878
2279
  skipContextFilter: forceInject,
1879
2280
  ledgerContext: input.ledgerContext,
1880
2281
  });
@@ -1891,10 +2292,11 @@ class AttendantInstance {
1891
2292
  const remainder = slashIdx2 === -1 ? '' : fact.entityKey.slice(slashIdx2);
1892
2293
  return { ...fact, entityKey: `${canonicalPersonalType}/${canonicalPersonalId}${remainder}` };
1893
2294
  });
2295
+ const structuredFacts = (0, hostMemoryFormatting_1.assignStructuredFactIds)(remappedFacts);
1894
2296
  watchedEntitiesChanged = this.updateWatchedEntities(observed.entitiesResolved?.map((entry) => entry.canonicalEntity) ?? []) || watchedEntitiesChanged;
1895
2297
  this.markSharedStateObserved(observeEntityHints.length > 0 ? observeEntityHints : freshState.entities);
1896
2298
  let reason = 'memory_needed_injected';
1897
- const shouldInject = remappedFacts.length > 0;
2299
+ const shouldInject = structuredFacts.length > 0;
1898
2300
  let searchSuggestion;
1899
2301
  if (!shouldInject) {
1900
2302
  const allAlreadyInContext = observed.totalFound > 0 && observed.alreadyPresent >= observed.totalFound;
@@ -1916,17 +2318,29 @@ class AttendantInstance {
1916
2318
  else if (forceInject) {
1917
2319
  reason = 'forced';
1918
2320
  }
2321
+ const memoryAttributions = shouldInject
2322
+ ? [
2323
+ this.addPendingMemoryAttribution({
2324
+ phase: phase === 'mid-turn' ? 'mid-turn' : 'pre-response',
2325
+ injectedKeys: structuredFacts.map((fact) => fact.entityKey),
2326
+ injectedEntryIds: structuredFacts
2327
+ .map((fact) => fact.knowledgeEntryId)
2328
+ .filter((value) => typeof value === 'number'),
2329
+ }),
2330
+ ]
2331
+ : [];
1919
2332
  const attendResult = {
1920
2333
  ...observed,
1921
- facts: remappedFacts,
1922
- shouldInject,
2334
+ facts: structuredFacts,
2335
+ shouldInject: structuredFacts.length > 0,
1923
2336
  reason,
1924
2337
  decision,
1925
2338
  bootstrap,
1926
2339
  searchSuggestion,
1927
2340
  complianceWarning,
1928
2341
  compliance,
1929
- usageGuidance: buildUsageGuidance('attend'),
2342
+ memoryAttributions,
2343
+ usageGuidance: buildUsageGuidance('attend', this.turnsWithoutWrite),
1930
2344
  };
1931
2345
  if (input.suppressEvents !== true) {
1932
2346
  (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
@@ -1940,6 +2354,7 @@ class AttendantInstance {
1940
2354
  contextCallCount: this.contextCallCount,
1941
2355
  shouldInject,
1942
2356
  attendReason: reason,
2357
+ injectionId: memoryAttributions[0]?.injectionId ?? null,
1943
2358
  advisoryDecisionMethod: decision.method,
1944
2359
  advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
1945
2360
  freshStateEntities: freshState.entities,
@@ -1956,7 +2371,11 @@ class AttendantInstance {
1956
2371
  metadata: this.buildEventMetadata({
1957
2372
  shouldInject,
1958
2373
  factCount: observed.facts.length,
2374
+ injectionId: memoryAttributions[0]?.injectionId ?? null,
1959
2375
  injectedKeys: observed.facts.map((fact) => fact.entityKey),
2376
+ injectedEntryIds: observed.facts
2377
+ .map((fact) => fact.knowledgeEntryId)
2378
+ .filter((value) => typeof value === 'number'),
1960
2379
  entitiesResolved: observed.entitiesResolved?.map((entry) => entry.canonicalEntity) ?? [],
1961
2380
  alreadyPresent: observed.alreadyPresent,
1962
2381
  totalFound: observed.totalFound,
@@ -1973,7 +2392,7 @@ class AttendantInstance {
1973
2392
  briefGeneratedAt: this.complianceUpdatedAt,
1974
2393
  };
1975
2394
  }
1976
- if (watchedEntitiesChanged || this.brief?.compliance !== compliance) {
2395
+ if (watchedEntitiesChanged || this.brief?.compliance !== compliance || memoryAttributions.length > 0) {
1977
2396
  await this.persistState();
1978
2397
  }
1979
2398
  (0, metrics_1.timeEnd)('attendant.attend_ms', t0);
@@ -2253,7 +2672,7 @@ ${detectionWindow}`,
2253
2672
  const allEntries = await (0, queries_1.findEntriesByEntity)(resolvedInfo.entityType, resolvedInfo.entityId);
2254
2673
  // Priority keys first
2255
2674
  const policyPriorityKeys = policy.observeKeyPriority?.[resolvedInfo.entityType] ?? [];
2256
- const priorityKeys = new Set([...policyPriorityKeys, ...requestedPriorityKeys]);
2675
+ const priorityKeys = new Set(expandContinuityPriorityKeys([...policyPriorityKeys, ...requestedPriorityKeys]));
2257
2676
  const priorityEntries = allEntries.filter((e) => priorityKeys.has(e.key));
2258
2677
  const remainingEntries = allEntries
2259
2678
  .filter((e) => !priorityKeys.has(e.key))
@@ -2264,13 +2683,20 @@ ${detectionWindow}`,
2264
2683
  || a.key.localeCompare(b.key));
2265
2684
  });
2266
2685
  const selectedEntries = [...priorityEntries, ...remainingEntries].slice(0, maxKeysPerEntity);
2686
+ const freshestEntry = allEntries
2687
+ .slice()
2688
+ .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime() || b.confidence - a.confidence)[0];
2689
+ if (freshestEntry && !selectedEntries.some((entry) => entry.id === freshestEntry.id)) {
2690
+ selectedEntries[selectedEntries.length - 1] = freshestEntry;
2691
+ }
2267
2692
  for (const entry of selectedEntries) {
2268
2693
  allFacts.push({
2269
- entityKey: `${resolvedInfo.entityType}/${resolvedInfo.entityId}/${entry.key}`,
2694
+ entityKey: `${resolvedInfo.entityType}/${resolvedInfo.entityId}/${normalizeContinuityKey(entry.key)}`,
2270
2695
  summary: entry.valueSummary,
2271
2696
  value: entry.valueRaw,
2272
2697
  confidence: entry.confidence,
2273
2698
  source: entry.source,
2699
+ lastUpdated: entry.updatedAt.toISOString(),
2274
2700
  entryId: entry.id,
2275
2701
  });
2276
2702
  }
@@ -2312,13 +2738,15 @@ ${detectionWindow}`,
2312
2738
  });
2313
2739
  (0, metrics_1.timeEnd)('attendant.observe_ms', t0);
2314
2740
  return {
2315
- facts: topFacts.map(({ entityKey, summary, value, confidence, source }) => ({
2741
+ facts: (0, hostMemoryFormatting_1.assignStructuredFactIds)(topFacts.map(({ entityKey, summary, value, confidence, source, lastUpdated, entryId }) => ({
2742
+ knowledgeEntryId: entryId,
2316
2743
  entityKey,
2317
2744
  summary,
2318
2745
  value,
2319
2746
  confidence,
2320
2747
  source,
2321
- })),
2748
+ lastUpdated,
2749
+ }))),
2322
2750
  entitiesDetected: Array.from(entitiesDetected),
2323
2751
  alreadyPresent,
2324
2752
  totalFound: allFacts.length,
@@ -2832,6 +3260,7 @@ If nothing is relevant, return: none`,
2832
3260
  this.brief = {
2833
3261
  ...this.brief,
2834
3262
  compliance: this.buildComplianceState(),
3263
+ pendingMemoryAttributions: this.pendingMemoryAttributions.map((entry) => ({ ...entry })),
2835
3264
  };
2836
3265
  await (0, client_1.getDb)().knowledgeEntry.upsert({
2837
3266
  where: {
@@ -2870,9 +3299,14 @@ If nothing is relevant, return: none`,
2870
3299
  this.advisoryLearningProfile = null;
2871
3300
  this.sharedStateObservedAt = state.briefGeneratedAt;
2872
3301
  this.attendsWithoutPersist = state.compliance?.counters.attendsWithoutPersist ?? 0;
3302
+ this.turnsWithoutWrite = state.compliance?.counters.turnsWithoutWrite ?? 0;
2873
3303
  this.consecutivePreResponseWithoutPost = state.compliance?.counters.consecutivePreResponseWithoutPost ?? 0;
3304
+ this.consecutiveUnusedMemoryInjections = state.compliance?.counters.consecutiveUnusedMemoryInjections ?? 0;
2874
3305
  this.lastAttendPhase = state.compliance?.counters.lastAttendPhase ?? undefined;
2875
3306
  this.complianceUpdatedAt = state.compliance?.lastUpdated ?? state.briefGeneratedAt;
3307
+ this.pendingMemoryAttributions = Array.isArray(state.pendingMemoryAttributions)
3308
+ ? state.pendingMemoryAttributions.map((entry) => ({ ...entry }))
3309
+ : [];
2876
3310
  this.brief = state;
2877
3311
  return state;
2878
3312
  }