iranti 0.2.51 → 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 (163) hide show
  1. package/README.md +30 -17
  2. package/dist/scripts/api-key-create.js +1 -1
  3. package/dist/scripts/api-key-list.js +1 -1
  4. package/dist/scripts/api-key-revoke.js +1 -1
  5. package/dist/scripts/claude-code-memory-hook.js +116 -30
  6. package/dist/scripts/codex-setup.js +86 -4
  7. package/dist/scripts/iranti-cli.js +1359 -57
  8. package/dist/scripts/iranti-mcp.js +578 -75
  9. package/dist/scripts/seed.js +11 -6
  10. package/dist/scripts/setup.js +1 -1
  11. package/dist/src/api/healthChecks.d.ts +29 -0
  12. package/dist/src/api/healthChecks.d.ts.map +1 -0
  13. package/dist/src/api/healthChecks.js +72 -0
  14. package/dist/src/api/healthChecks.js.map +1 -0
  15. package/dist/src/api/middleware/validation.d.ts +22 -0
  16. package/dist/src/api/middleware/validation.d.ts.map +1 -1
  17. package/dist/src/api/middleware/validation.js +93 -3
  18. package/dist/src/api/middleware/validation.js.map +1 -1
  19. package/dist/src/api/routes/knowledge.d.ts.map +1 -1
  20. package/dist/src/api/routes/knowledge.js +53 -0
  21. package/dist/src/api/routes/knowledge.js.map +1 -1
  22. package/dist/src/api/routes/memory.d.ts.map +1 -1
  23. package/dist/src/api/routes/memory.js +73 -9
  24. package/dist/src/api/routes/memory.js.map +1 -1
  25. package/dist/src/api/server.js +38 -43
  26. package/dist/src/api/server.js.map +1 -1
  27. package/dist/src/attendant/AttendantInstance.d.ts +135 -2
  28. package/dist/src/attendant/AttendantInstance.d.ts.map +1 -1
  29. package/dist/src/attendant/AttendantInstance.js +1836 -93
  30. package/dist/src/attendant/AttendantInstance.js.map +1 -1
  31. package/dist/src/attendant/index.d.ts +1 -1
  32. package/dist/src/attendant/index.d.ts.map +1 -1
  33. package/dist/src/attendant/index.js +1 -1
  34. package/dist/src/attendant/index.js.map +1 -1
  35. package/dist/src/attendant/registry.d.ts.map +1 -1
  36. package/dist/src/attendant/registry.js +2 -0
  37. package/dist/src/attendant/registry.js.map +1 -1
  38. package/dist/src/chat/index.d.ts +23 -0
  39. package/dist/src/chat/index.d.ts.map +1 -1
  40. package/dist/src/chat/index.js +111 -22
  41. package/dist/src/chat/index.js.map +1 -1
  42. package/dist/src/generated/prisma/browser.d.ts +5 -0
  43. package/dist/src/generated/prisma/browser.d.ts.map +1 -1
  44. package/dist/src/generated/prisma/client.d.ts +5 -0
  45. package/dist/src/generated/prisma/client.d.ts.map +1 -1
  46. package/dist/src/generated/prisma/commonInputTypes.d.ts +48 -0
  47. package/dist/src/generated/prisma/commonInputTypes.d.ts.map +1 -1
  48. package/dist/src/generated/prisma/internal/class.d.ts +11 -0
  49. package/dist/src/generated/prisma/internal/class.d.ts.map +1 -1
  50. package/dist/src/generated/prisma/internal/class.js +4 -4
  51. package/dist/src/generated/prisma/internal/class.js.map +1 -1
  52. package/dist/src/generated/prisma/internal/prismaNamespace.d.ts +92 -1
  53. package/dist/src/generated/prisma/internal/prismaNamespace.d.ts.map +1 -1
  54. package/dist/src/generated/prisma/internal/prismaNamespace.js +17 -2
  55. package/dist/src/generated/prisma/internal/prismaNamespace.js.map +1 -1
  56. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts +16 -0
  57. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.d.ts.map +1 -1
  58. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js +17 -2
  59. package/dist/src/generated/prisma/internal/prismaNamespaceBrowser.js.map +1 -1
  60. package/dist/src/generated/prisma/models/StaffEvent.d.ts +1184 -0
  61. package/dist/src/generated/prisma/models/StaffEvent.d.ts.map +1 -0
  62. package/dist/src/generated/prisma/models/StaffEvent.js +3 -0
  63. package/dist/src/generated/prisma/models/StaffEvent.js.map +1 -0
  64. package/dist/src/generated/prisma/models.d.ts +1 -0
  65. package/dist/src/generated/prisma/models.d.ts.map +1 -1
  66. package/dist/src/lib/assistantCheckpoint.d.ts +21 -0
  67. package/dist/src/lib/assistantCheckpoint.d.ts.map +1 -0
  68. package/dist/src/lib/assistantCheckpoint.js +143 -0
  69. package/dist/src/lib/assistantCheckpoint.js.map +1 -0
  70. package/dist/src/lib/autoRemember.d.ts +15 -0
  71. package/dist/src/lib/autoRemember.d.ts.map +1 -1
  72. package/dist/src/lib/autoRemember.js +433 -71
  73. package/dist/src/lib/autoRemember.js.map +1 -1
  74. package/dist/src/lib/cliHelpCatalog.d.ts.map +1 -1
  75. package/dist/src/lib/cliHelpCatalog.js +23 -11
  76. package/dist/src/lib/cliHelpCatalog.js.map +1 -1
  77. package/dist/src/lib/cliHelpRenderer.d.ts +1 -0
  78. package/dist/src/lib/cliHelpRenderer.d.ts.map +1 -1
  79. package/dist/src/lib/cliHelpRenderer.js +4 -0
  80. package/dist/src/lib/cliHelpRenderer.js.map +1 -1
  81. package/dist/src/lib/commandErrors.d.ts +5 -1
  82. package/dist/src/lib/commandErrors.d.ts.map +1 -1
  83. package/dist/src/lib/commandErrors.js +250 -17
  84. package/dist/src/lib/commandErrors.js.map +1 -1
  85. package/dist/src/lib/createFirstPartyIranti.d.ts.map +1 -1
  86. package/dist/src/lib/createFirstPartyIranti.js +1 -0
  87. package/dist/src/lib/createFirstPartyIranti.js.map +1 -1
  88. package/dist/src/lib/dbStaffEventEmitter.d.ts +2 -0
  89. package/dist/src/lib/dbStaffEventEmitter.d.ts.map +1 -1
  90. package/dist/src/lib/dbStaffEventEmitter.js +15 -0
  91. package/dist/src/lib/dbStaffEventEmitter.js.map +1 -1
  92. package/dist/src/lib/hostMemoryFormatting.d.ts +25 -0
  93. package/dist/src/lib/hostMemoryFormatting.d.ts.map +1 -0
  94. package/dist/src/lib/hostMemoryFormatting.js +55 -0
  95. package/dist/src/lib/hostMemoryFormatting.js.map +1 -0
  96. package/dist/src/lib/issueFacts.d.ts +37 -0
  97. package/dist/src/lib/issueFacts.d.ts.map +1 -0
  98. package/dist/src/lib/issueFacts.js +72 -0
  99. package/dist/src/lib/issueFacts.js.map +1 -0
  100. package/dist/src/lib/llm.d.ts +8 -0
  101. package/dist/src/lib/llm.d.ts.map +1 -1
  102. package/dist/src/lib/llm.js +33 -0
  103. package/dist/src/lib/llm.js.map +1 -1
  104. package/dist/src/lib/packageRoot.d.ts +2 -0
  105. package/dist/src/lib/packageRoot.d.ts.map +1 -0
  106. package/dist/src/lib/packageRoot.js +22 -0
  107. package/dist/src/lib/packageRoot.js.map +1 -0
  108. package/dist/src/lib/projectLearning.d.ts +21 -0
  109. package/dist/src/lib/projectLearning.d.ts.map +1 -0
  110. package/dist/src/lib/projectLearning.js +357 -0
  111. package/dist/src/lib/projectLearning.js.map +1 -0
  112. package/dist/src/lib/protocolEnforcement.d.ts +29 -0
  113. package/dist/src/lib/protocolEnforcement.d.ts.map +1 -0
  114. package/dist/src/lib/protocolEnforcement.js +124 -0
  115. package/dist/src/lib/protocolEnforcement.js.map +1 -0
  116. package/dist/src/lib/providers/claude.js +1 -1
  117. package/dist/src/lib/providers/claude.js.map +1 -1
  118. package/dist/src/lib/router.js +1 -1
  119. package/dist/src/lib/router.js.map +1 -1
  120. package/dist/src/lib/runtimeEnv.d.ts.map +1 -1
  121. package/dist/src/lib/runtimeEnv.js +8 -3
  122. package/dist/src/lib/runtimeEnv.js.map +1 -1
  123. package/dist/src/lib/scaffoldCloseout.d.ts +27 -0
  124. package/dist/src/lib/scaffoldCloseout.d.ts.map +1 -0
  125. package/dist/src/lib/scaffoldCloseout.js +139 -0
  126. package/dist/src/lib/scaffoldCloseout.js.map +1 -0
  127. package/dist/src/lib/semanticFactTags.d.ts +10 -0
  128. package/dist/src/lib/semanticFactTags.d.ts.map +1 -0
  129. package/dist/src/lib/semanticFactTags.js +166 -0
  130. package/dist/src/lib/semanticFactTags.js.map +1 -0
  131. package/dist/src/lib/sessionLedger.d.ts +94 -0
  132. package/dist/src/lib/sessionLedger.d.ts.map +1 -0
  133. package/dist/src/lib/sessionLedger.js +997 -0
  134. package/dist/src/lib/sessionLedger.js.map +1 -0
  135. package/dist/src/lib/sharedStateInvalidation.d.ts +10 -0
  136. package/dist/src/lib/sharedStateInvalidation.d.ts.map +1 -0
  137. package/dist/src/lib/sharedStateInvalidation.js +184 -0
  138. package/dist/src/lib/sharedStateInvalidation.js.map +1 -0
  139. package/dist/src/lib/staffEventsTable.d.ts +3 -0
  140. package/dist/src/lib/staffEventsTable.d.ts.map +1 -0
  141. package/dist/src/lib/staffEventsTable.js +58 -0
  142. package/dist/src/lib/staffEventsTable.js.map +1 -0
  143. package/dist/src/librarian/index.d.ts.map +1 -1
  144. package/dist/src/librarian/index.js +113 -2
  145. package/dist/src/librarian/index.js.map +1 -1
  146. package/dist/src/library/client.d.ts +6 -1
  147. package/dist/src/library/client.d.ts.map +1 -1
  148. package/dist/src/library/client.js +21 -7
  149. package/dist/src/library/client.js.map +1 -1
  150. package/dist/src/library/embeddings.d.ts +9 -1
  151. package/dist/src/library/embeddings.d.ts.map +1 -1
  152. package/dist/src/library/embeddings.js +28 -3
  153. package/dist/src/library/embeddings.js.map +1 -1
  154. package/dist/src/library/queries.d.ts.map +1 -1
  155. package/dist/src/library/queries.js +263 -46
  156. package/dist/src/library/queries.js.map +1 -1
  157. package/dist/src/sdk/index.d.ts +52 -1
  158. package/dist/src/sdk/index.d.ts.map +1 -1
  159. package/dist/src/sdk/index.js +546 -98
  160. package/dist/src/sdk/index.js.map +1 -1
  161. package/package.json +24 -3
  162. package/prisma/migrations/20260331101500_add_staff_events_ledger/migration.sql +24 -0
  163. package/prisma/schema.prisma +22 -0
@@ -5,15 +5,22 @@ 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");
12
+ const queries_2 = require("../library/queries");
11
13
  const relationships_1 = require("../library/relationships");
12
14
  const entity_resolution_1 = require("../library/entity-resolution");
15
+ const librarian_1 = require("../librarian");
13
16
  const client_1 = require("../library/client");
14
17
  const metrics_1 = require("../lib/metrics");
15
18
  const getPolicy_1 = require("../librarian/getPolicy");
16
19
  const autoRemember_1 = require("../lib/autoRemember");
20
+ const semanticFactTags_1 = require("../lib/semanticFactTags");
21
+ const sessionLedger_1 = require("../lib/sessionLedger");
22
+ const sharedStateInvalidation_1 = require("../lib/sharedStateInvalidation");
23
+ const hostMemoryFormatting_1 = require("../lib/hostMemoryFormatting");
17
24
  // ─── Constants ───────────────────────────────────────────────────────────────
18
25
  const ATTENDANT_RULES_QUERY = {
19
26
  entityType: 'system',
@@ -22,40 +29,117 @@ const ATTENDANT_RULES_QUERY = {
22
29
  };
23
30
  exports.DEFAULT_ATTENDANT_OPERATING_RULES = [
24
31
  'Serve one external agent only; optimize for that agent keeping task context coherent across turns and sessions.',
25
- 'At session start or when the task changes, run handshake to load operating rules, task context, and the most relevant shared memory.',
32
+ 'ACKNOWLEDGE: This is strictly mandatory — you MUST state the following before proceeding with any user request, and no work should begin until it is stated: "I acknowledge that Iranti is a hive mind. I have read the Iranti operating rules and the protocols for iranti_handshake, iranti_attend, iranti_write, iranti_checkpoint, and iranti_remember_response, and I pledge to use them to persist what I find, what works, what fails, what changes, and what happens next so later sessions do not have to rediscover it." Skipping this acknowledgment is non-compliant regardless of context.',
33
+ 'SESSION START: Call iranti_handshake before responding to the first user message. After context compaction, call iranti_handshake again before responding to the next message.',
34
+ "ATTEND PROTOCOL — follow this sequence every turn: (1) Call iranti_attend before responding to the user message — pass phase='pre-response' to enable precise compliance tracking. (2) Call iranti_attend before any lookup where Iranti might already hold the answer — this includes Read, Grep, Glob, WebSearch, WebFetch, and Bash commands used as factual basis for a decision. (3) Call iranti_attend after any such lookup to check new findings against stored memory and decide what to inject, write, or checkpoint. (4) Call iranti_write after every Edit or Write tool call — file changes are always durable, record what changed, why, and what the file now does. (5) Call iranti_write after any Bash command that reveals system state — build results, test outcomes, container state, port availability, environment facts, errors. (6) Call iranti_write after any WebSearch or WebFetch that surfaces confirmed external facts. (7) Call iranti_write after any subagent (Agent tool) completes — subagent findings are invisible to the hive mind otherwise. (8) Call iranti_attend after every response without exception — pass phase='post-response'. Even short or conversational replies may contain durable decisions, confirmed facts, or next steps. Assess for write or checkpoint before the next turn. (9) Call iranti_attend again when new knowledge should change what is loaded for the next step.",
26
35
  'Treat Iranti as the default shared working-memory layer. Keep using your own private notes if you want, but prefer Iranti for anything another session, another agent, or a later handoff may need.',
27
36
  'Before answering recall-style questions about remembered preferences, decisions, blockers, next steps, prior project state, or earlier findings, consult Iranti instead of guessing.',
37
+ 'If a recall-style lookup returns no facts, do not treat empty as confirmation of absence — try at least one alternative retrieval angle before concluding the fact is not stored: switch between exact query and search, try a different entity path or key fragment, or rephrase the search term. Absence is confirmed only after two distinct retrieval attempts with different angles both return empty.',
28
38
  'Before making or repeating architectural, product, workflow, or debugging decisions, check Iranti for earlier decisions, constraints, blockers, and validated environment details.',
29
39
  'Use exact query when the entity and key are known. Use search or attend when the fact must be discovered from shared memory.',
30
- 'Persist durable knowledge when it is learned or confirmed: decisions, blockers, next steps, owners, stable preferences, project constraints, important file purposes, and validated environment details.',
40
+ 'Persist durable knowledge when it is learned or confirmed: decisions, blockers, next steps, owners, stable preferences, project constraints, important file purposes, validated environment details, what worked, what failed, and what remains risky.',
41
+ 'Write facts with the depth of someone who built the system — include what the thing does, why it exists, how it connects to other parts, and what would break or change if it were removed. A fact that reads "file X was edited" is insufficient; "file X controls Y because Z, edited to fix W" is the target. Iranti should accumulate enough detail that any agent reading its memory feels like it built the repo.',
31
42
  'When a file is created, renamed, moved, deleted, or substantially repurposed, capture that change and what the file is for whenever the state will matter to another agent or a later session.',
32
43
  'When a task reaches a useful checkpoint, store the current step, next step, open risks, and any important artifacts or paths so another agent can resume without reconstructing context from scratch.',
33
44
  'When an approach fails and the failure or workaround is likely to matter later, store the failed path and the chosen alternative route as durable memory.',
34
- 'Use iranti_write for durable facts, iranti_ingest for stable source material worth chunking, and iranti_remember_response for strict assistant summaries such as next steps or blockers.',
35
- 'Do not save every turn. Skip ephemeral chatter, speculative thoughts, or transient execution noise that will degrade retrieval quality later.',
45
+ 'Use iranti_checkpoint for active shared progress, iranti_write for durable facts, iranti_ingest for stable source material worth chunking, and iranti_remember_response for strict assistant summaries such as next steps or blockers.',
46
+ 'CHECKPOINT PROTOCOL: Call iranti_checkpoint (1) when completing a task, (2) when shifting to a new task mid-session, and (3) at any natural pause point where another session should resume — not only when saving facts with iranti_write. A checkpoint not written means the next handshake recovers from stale data, and a long run without structured writes/checkpoints is non-compliant for Iranti. Write checkpoints like the best possible commit message but with more detail — lead with the why (what problem this solved, what decision was made, what changed and why it matters), then add structured recovery context: current step, next step, what worked, what failed, open risks, and file changes. A checkpoint that reads "did some edits" is non-compliant; one that reads "fixed missing docker dependency in cofactor instance.json — container name was never recorded at setup so iranti run silently skipped docker start; added iranti_cofactor_db dependency, verified against docker ps, control panel start will now auto-boot the container" is the target.',
47
+ 'Do not save every turn. Skip idle chatter and transient execution noise. Do save design options, architectural proposals, and considered-but-not-yet-decided directions with their reasoning — a future session should not have to re-explore ground already covered, even when no final decision was made. Do not skip discoveries, failed paths, validations, file changes, risks, or next steps that another session would otherwise have to rediscover.',
36
48
  'Deliver a compressed working-memory brief, not the full knowledge base. Load only what is relevant to the current task.',
37
49
  'Reconvene or attend again when context shifts, when the visible window is missing needed facts, or when a different entity becomes relevant.',
38
50
  'If context gets stale or the task has gone long enough that reasoning may drift, re-read the operating rules from the Staff Namespace before proceeding.',
39
51
  ];
40
52
  const CONTEXT_RECOVERY_THRESHOLD = 20; // LLM calls before context recovery
41
53
  const SESSION_INTERRUPTION_TTL_MS = 5 * 60 * 1000;
54
+ const PERSISTENCE_WARNING_THRESHOLD = 3;
55
+ const PERSISTENCE_NON_COMPLIANT_THRESHOLD = 5;
42
56
  const ENTITY_DETECTION_WINDOW_CHARS = 1500;
43
57
  const MIN_ENTITY_CONFIDENCE = 0.75;
44
58
  const MEMORY_DECISION_CONTEXT_WINDOW_CHARS = 2000;
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
+ };
65
+ const ATTEND_EXPECTED_CALL_SEQUENCE = [
66
+ 'Call iranti_handshake at session start and again after context compaction.',
67
+ "Call iranti_attend(phase='pre-response') before replying to the user.",
68
+ 'Call iranti_attend before any lookup where Iranti might already hold the answer — Read, Grep, Glob, WebSearch, WebFetch, Bash-as-factual-basis.',
69
+ 'Call iranti_attend again after any such lookup when new findings may affect what to inject, write, or checkpoint.',
70
+ 'Call iranti_write after every Edit or Write tool call — file changes are always durable.',
71
+ 'Call iranti_write after Bash commands that reveal system state, after WebSearch/WebFetch with confirmed facts, and after any subagent completes.',
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.",
73
+ 'Call iranti_attend again when the new knowledge should change what is loaded next.',
74
+ ];
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.';
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
+ }
45
91
  const MEMORY_NEED_POSITIVE_PATTERNS = [
46
92
  /\bwhat(?:'s| is| was)?\s+my\b/i,
47
93
  /\bdo you remember\b/i,
48
94
  /\bremind me\b/i,
95
+ /\bbring me up to speed\b/i,
96
+ /\bcatch me up\b/i,
97
+ /\brecap\b/i,
98
+ /\bwhere did we leave off\b/i,
99
+ /\bwhere are we\b/i,
100
+ /\bwhat did we learn\b/i,
101
+ /\bwhat did we decide\b/i,
102
+ /\bwhat do we know\b/i,
103
+ /\bnext step\b/i,
104
+ /\bwhat(?:'s| is)?\s+next\b/i,
105
+ /\b(?:what|which)\s+(?:bugs?|issues?|defects?|tasks?|blockers?|risks?)\s+(?:are\s+)?(?:left|open|remaining)\b/i,
106
+ /\bwhat\s+(?:changed|worked|failed)\b/i,
107
+ /\b(?:current\s+)?status\b/i,
108
+ /\b(?:current\s+)?progress\b/i,
109
+ /\b(?:summary|summarize|overview)\b/i,
49
110
  /\bmy\s+(?:favorite|favourite|name|email|phone|address|city|country|movie|snack|color|colour)\b/i,
50
111
  /\bwe decided\b/i,
51
112
  /\bearlier\b/i,
52
113
  /\bprevious(?:ly)?\b/i,
53
114
  /\bagain\b/i,
115
+ // Common imperative work-task prefixes — short messages like "fix it", "add tests",
116
+ // "help me debug", "explain this" are almost always project-contextual and benefit from
117
+ // memory injection. Catching these here prevents fall-through to the LLM classifier and
118
+ // the classification_parse_failed_default_false silent miss.
119
+ /^\s*(?:fix|debug|refactor|implement|add|update|change|remove|delete|create|write|check|review|test|run|deploy|build|enable|disable|configure|set up|setup)\b/i,
120
+ /\bhelp\s+me\b/i,
121
+ /\bexplain\s+(?:this|that|how|why|what)\b/i,
122
+ /\bwhat\s+(?:is|are|was|were)\s+(?:the|a|an|this|that|my|our|its)\b/i,
123
+ /\bhow\s+(?:do|does|did|should|can|could|would|to)\b/i,
124
+ /\bwhy\s+(?:is|are|was|were|did|does|do|not)\b/i,
125
+ // Technical vocabulary — file paths, function-call syntax, camelCase/snake_case identifiers,
126
+ // and dot-notation references are strong signals that the message is project-bound.
127
+ /(?:\/[\w.\-]+){1,}|[\w]+\.[a-z]{1,5}\b/i,
128
+ /\b[a-z][a-zA-Z0-9]*(?:[A-Z][a-zA-Z0-9]*)+\b/, // camelCase
129
+ /\b[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)+\b/, // snake_case
130
+ /\b\w+\s*\(/, // function call syntax
54
131
  ];
55
132
  const MEMORY_NEED_NEGATIVE_PATTERNS = [
56
133
  /^\s*(hi|hello|hey|yo|sup|good (?:morning|afternoon|evening))\b[!.?\s]*$/i,
57
134
  /^\s*(thanks|thank you|cool|great|nice)\b[!.?\s]*$/i,
58
135
  ];
136
+ const MEMORY_PARSE_FAILURE_PROJECT_CUE_PATTERNS = [
137
+ /\b(?:status|progress|summary|summar(?:y|ize)|recap|overview|state)\b/i,
138
+ /\b(?:decision|decisions|findings?|artifacts?|changes?|work|implementation|architecture|code(?:base)?|repo|repository)\b/i,
139
+ /\b(?:deployment|setup|bug|bugs|issue|issues|defect|defects|task|tasks|blocker|blockers|risk|risks)\b/i,
140
+ /\bwhat\s+(?:did|do|have)\s+we\b/i,
141
+ /\bwhere\s+do\s+we\s+stand\b/i,
142
+ ];
59
143
  const EXPLICIT_TASK_PREFIX_PATTERNS = [
60
144
  /^\s*general session\s*[:\-]\s*/i,
61
145
  /^\s*general session assistance\s*(?:for)?\s*/i,
@@ -69,6 +153,20 @@ const WEAK_EXPLICIT_TASK_PATTERNS = [
69
153
  /^assistance$/i,
70
154
  /^help$/i,
71
155
  ];
156
+ const WATCHED_ENTITY_PROMPT_PATTERNS = [
157
+ /\bcontinue\b/i,
158
+ /\bresume\b/i,
159
+ /\bpick up\b/i,
160
+ /\bwhat(?:'s| is)?\s+next\b/i,
161
+ /\bwhat(?:'s| is)?\s+the\s+next\s+step\b/i,
162
+ /\bwhat(?:'s| is)?\s+the\s+status\b/i,
163
+ /\bstatus\b/i,
164
+ /\bprogress\b/i,
165
+ /\brecap\b/i,
166
+ /\bwhat\s+changed\b/i,
167
+ /\bwhat\s+did\s+you\s+change\b/i,
168
+ /\bwhere\s+were\s+we\b/i,
169
+ ];
72
170
  function normalizeExplicitTask(task) {
73
171
  if (typeof task !== 'string')
74
172
  return null;
@@ -118,6 +216,162 @@ function formatOperatingRulesText(rawValue, summary, fallbackRules = exports.DEF
118
216
  ...mergedRules.map((rule) => `- ${rule}`),
119
217
  ].join('\n');
120
218
  }
219
+ function summarizeLedgerLearning(entry) {
220
+ return `Recent ledger learning: ${entry.summary}`;
221
+ }
222
+ function toLedgerWorkingMemoryEntries(entries) {
223
+ return entries.map((entry, index) => ({
224
+ entityKey: `system/session_ledger/recent_learning_${index + 1}`,
225
+ summary: summarizeLedgerLearning(entry),
226
+ confidence: 100,
227
+ source: 'session_ledger',
228
+ lastUpdated: entry.timestamp,
229
+ }));
230
+ }
231
+ function mergeWorkingMemoryWithLedger(entries, learnings) {
232
+ const retained = entries.filter((entry) => !entry.entityKey.startsWith(LEDGER_WORKING_MEMORY_PREFIX));
233
+ return learnings.length > 0
234
+ ? [...retained, ...toLedgerWorkingMemoryEntries(learnings)]
235
+ : retained;
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
+ }
314
+ function formatMissingWriteCategories(categories) {
315
+ const labelMap = {
316
+ findings: 'what you found',
317
+ validated_results: 'what worked',
318
+ failed_paths: 'what failed',
319
+ file_changes: 'what changed',
320
+ risks_and_next_steps: 'what remains risky and what happens next',
321
+ };
322
+ const labels = Array.from(new Set(categories.map((category) => labelMap[category] ?? category)));
323
+ if (labels.length === 0)
324
+ return 'what you found and what happens next';
325
+ if (labels.length === 1)
326
+ return labels[0];
327
+ if (labels.length === 2)
328
+ return `${labels[0]} and ${labels[1]}`;
329
+ return `${labels.slice(0, -1).join(', ')}, and ${labels[labels.length - 1]}`;
330
+ }
331
+ function applyAdvisoryOperatingRules(operatingRules, profile) {
332
+ const reminder = profile?.checkpointReminder?.trim();
333
+ const categories = profile?.missingWriteCategories ?? [];
334
+ let nextRules = operatingRules;
335
+ if (reminder && !nextRules.includes(reminder)) {
336
+ nextRules = `${nextRules}\n- ${reminder}`;
337
+ }
338
+ if (categories.length > 0) {
339
+ const categoryRule = `Compliance follow-up: before the next pause, persist ${formatMissingWriteCategories(categories)} as structured durable memory when applicable, not just a broad summary.`;
340
+ if (!nextRules.includes(categoryRule)) {
341
+ nextRules = `${nextRules}\n- ${categoryRule}`;
342
+ }
343
+ }
344
+ return nextRules;
345
+ }
346
+ function advisoryTaskTokens(taskType) {
347
+ if (!taskType)
348
+ return [];
349
+ return Array.from(new Set(taskType
350
+ .toLowerCase()
351
+ .split(/[^a-z0-9_]+/)
352
+ .map((token) => token.trim())
353
+ .filter((token) => token.length >= 4)));
354
+ }
355
+ function buildUsageGuidance(tool) {
356
+ return {
357
+ tool,
358
+ reminder: ATTEND_USAGE_REMINDER,
359
+ expectedCallSequence: ATTEND_EXPECTED_CALL_SEQUENCE,
360
+ note: tool === 'observe'
361
+ ? OBSERVE_USAGE_NOTE
362
+ : 'After using attend() and any retrieved facts, persist durable learnings with iranti_write and shared progress with iranti_checkpoint when applicable.',
363
+ };
364
+ }
365
+ function messageHasAdvisoryCue(message, taskType) {
366
+ const normalized = normalizeMessage(message);
367
+ if (!normalized)
368
+ return false;
369
+ if (/\b(next|status|blocker|owner|risk|issue|bug|weakness|problem|continue|resume|pickup|tackle|ship|release)\b/i.test(normalized)) {
370
+ return true;
371
+ }
372
+ const messageTokens = new Set(advisoryTaskTokens(normalized));
373
+ return advisoryTaskTokens(taskType).some((token) => messageTokens.has(token));
374
+ }
121
375
  function collectBackfillCandidates(messages) {
122
376
  const deduped = new Map();
123
377
  for (const message of messages) {
@@ -140,7 +394,7 @@ function collectBackfillCandidates(messages) {
140
394
  return Array.from(deduped.values());
141
395
  }
142
396
  function buildBackfillSuggestion(context, workingMemory) {
143
- const recentMessages = context.recentMessages
397
+ const recentMessages = (context.recentMessages ?? [])
144
398
  .map((message) => message.trim())
145
399
  .filter(Boolean);
146
400
  if (recentMessages.length === 0)
@@ -195,6 +449,85 @@ function buildAttendBootstrapMessages(latestMessage, currentContext) {
195
449
  }
196
450
  return out.slice(-6);
197
451
  }
452
+ function tokenizePresenceText(value) {
453
+ return value
454
+ .toLowerCase()
455
+ .split(/[^a-z0-9_]+/)
456
+ .map((token) => token.trim())
457
+ .filter((token) => token.length > 4);
458
+ }
459
+ function countPresenceMatches(contextLower, tokens) {
460
+ return tokens.filter((token) => contextLower.includes(token)).length;
461
+ }
462
+ function factAlreadyPresentInContext(contextLower, fact) {
463
+ const summaryLower = fact.summary.toLowerCase().trim();
464
+ if (summaryLower.length >= 24 && contextLower.includes(summaryLower)) {
465
+ return true;
466
+ }
467
+ const [entityType = '', entityId = '', key = ''] = fact.entityKey.split('/');
468
+ const entityTokens = new Set([
469
+ ...tokenizePresenceText(entityType),
470
+ ...tokenizePresenceText(entityId),
471
+ ...tokenizePresenceText(key),
472
+ ]);
473
+ const summaryTokens = tokenizePresenceText(fact.summary);
474
+ const meaningfulTokens = summaryTokens.filter((token) => !entityTokens.has(token));
475
+ const tokensToCheck = meaningfulTokens.length > 0 ? meaningfulTokens : summaryTokens;
476
+ if (tokensToCheck.length === 0) {
477
+ return false;
478
+ }
479
+ const matches = countPresenceMatches(contextLower, tokensToCheck);
480
+ return matches >= Math.ceil(tokensToCheck.length * 0.6);
481
+ }
482
+ function normalizeWatchedEntities(values) {
483
+ const seen = new Set();
484
+ const normalized = [];
485
+ for (const value of values ?? []) {
486
+ if (typeof value !== 'string')
487
+ continue;
488
+ const trimmed = value.trim();
489
+ if (!trimmed || !trimmed.includes('/') || seen.has(trimmed))
490
+ continue;
491
+ seen.add(trimmed);
492
+ normalized.push(trimmed);
493
+ if (normalized.length >= 8)
494
+ break;
495
+ }
496
+ return normalized;
497
+ }
498
+ function shouldUseWatchedEntitiesForPrompt(latestMessage) {
499
+ const normalized = normalizeMessage(latestMessage);
500
+ if (!normalized)
501
+ return false;
502
+ if (WATCHED_ENTITY_PROMPT_PATTERNS.some((pattern) => pattern.test(normalized))) {
503
+ return true;
504
+ }
505
+ return (0, autoRemember_1.classifyMemoryScope)(normalized) === 'project';
506
+ }
507
+ function inferContextWatchedEntities(task, recentMessages) {
508
+ const exact = normalizeWatchedEntities([
509
+ ...extractExactEntityReferences(task),
510
+ ...recentMessages.flatMap((message) => extractExactEntityReferences(message)),
511
+ ]);
512
+ if (exact.length > 0) {
513
+ return exact;
514
+ }
515
+ const combined = [task, ...recentMessages]
516
+ .map((message) => normalizeMessage(message))
517
+ .filter(Boolean)
518
+ .join('\n');
519
+ const scope = (0, autoRemember_1.classifyMemoryScope)(combined);
520
+ if (scope === 'personal') {
521
+ return (0, autoRemember_1.getPersonalRecallEntities)();
522
+ }
523
+ if (scope === 'project') {
524
+ const configured = (0, autoRemember_1.getProjectMemoryEntity)();
525
+ if (configured) {
526
+ return [configured];
527
+ }
528
+ }
529
+ return [];
530
+ }
198
531
  async function readPersistedBriefForAgent(agentId) {
199
532
  const entry = await (0, client_1.getDb)().knowledgeEntry.findUnique({
200
533
  where: {
@@ -229,9 +562,78 @@ function buildCheckpointSummary(checkpoint) {
229
562
  nextStep: checkpoint.checkpoint.nextStep ?? null,
230
563
  openRiskCount: Array.isArray(checkpoint.checkpoint.openRisks) ? checkpoint.checkpoint.openRisks.length : 0,
231
564
  entityTargetCount: Array.isArray(checkpoint.checkpoint.entityTargets) ? checkpoint.checkpoint.entityTargets.length : 0,
565
+ actionCount: Array.isArray(checkpoint.checkpoint.actions) ? checkpoint.checkpoint.actions.length : 0,
566
+ };
567
+ }
568
+ function buildSessionComplianceState(input) {
569
+ const pendingPostResponse = input.lastAttendPhase === 'pre-response';
570
+ const issues = [];
571
+ if (input.consecutivePreResponseWithoutPost > 0) {
572
+ issues.push({
573
+ code: 'missing_post_response_attend',
574
+ severity: 'error',
575
+ count: input.consecutivePreResponseWithoutPost,
576
+ message: 'The previous turn has not been closed with iranti_attend(phase=\'post-response\').',
577
+ requiredAction: 'Call iranti_attend(phase=\'post-response\') to close the prior turn, then persist any durable findings before the next pre-response attend.',
578
+ });
579
+ }
580
+ if (input.attendsWithoutPersist >= PERSISTENCE_WARNING_THRESHOLD) {
581
+ const severity = input.attendsWithoutPersist >= PERSISTENCE_NON_COMPLIANT_THRESHOLD ? 'error' : 'warn';
582
+ issues.push({
583
+ code: 'missing_durable_persistence',
584
+ severity,
585
+ count: input.attendsWithoutPersist,
586
+ message: `There have been ${input.attendsWithoutPersist} attend calls since the last iranti_write or iranti_checkpoint.`,
587
+ requiredAction: 'Persist durable findings with iranti_write or iranti_checkpoint before the next turn if new knowledge, validation, or file changes occurred.',
588
+ });
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
+ }
602
+ let status = 'healthy';
603
+ if (issues.some((issue) => issue.severity === 'error')) {
604
+ status = 'non_compliant';
605
+ }
606
+ else if (issues.length > 0) {
607
+ status = 'degraded';
608
+ }
609
+ const summary = status === 'healthy'
610
+ ? pendingPostResponse
611
+ ? 'Lifecycle is currently in progress and waiting for a post-response attend.'
612
+ : 'Lifecycle is currently compliant.'
613
+ : status === 'degraded'
614
+ ? input.consecutiveUnusedMemoryInjections > 0
615
+ ? 'Lifecycle is degraded: injected memory was surfaced but not used.'
616
+ : 'Lifecycle is degraded: persistence breadcrumbs are lagging.'
617
+ : input.consecutivePreResponseWithoutPost > 0
618
+ ? 'Lifecycle is non-compliant: the previous turn is still missing a post-response attend.'
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.';
622
+ return {
623
+ status,
624
+ summary,
625
+ issues,
626
+ lastUpdated: input.lastUpdated ?? new Date().toISOString(),
627
+ counters: {
628
+ attendsWithoutPersist: input.attendsWithoutPersist,
629
+ consecutivePreResponseWithoutPost: input.consecutivePreResponseWithoutPost,
630
+ consecutiveUnusedMemoryInjections: input.consecutiveUnusedMemoryInjections,
631
+ pendingPostResponse,
632
+ lastAttendPhase: input.lastAttendPhase ?? null,
633
+ },
232
634
  };
233
635
  }
234
- function summarizeSessionState(agentId, checkpoint, persistedBriefGeneratedAt) {
636
+ function summarizeSessionState(agentId, checkpoint, persistedBriefGeneratedAt, compliance = null) {
235
637
  const hasCheckpoint = Boolean(checkpoint);
236
638
  const lastHeartbeatAt = checkpoint?.lastHeartbeatAt ?? null;
237
639
  const isStale = Boolean(checkpoint
@@ -259,6 +661,7 @@ function summarizeSessionState(agentId, checkpoint, persistedBriefGeneratedAt) {
259
661
  isStale,
260
662
  persistedBriefGeneratedAt,
261
663
  checkpointSummary: buildCheckpointSummary(checkpoint),
664
+ compliance,
262
665
  };
263
666
  }
264
667
  function heuristicEntityId(name) {
@@ -361,6 +764,26 @@ function extractFallbackCandidates(text) {
361
764
  }
362
765
  return candidates;
363
766
  }
767
+ function extractExactEntityReferences(text) {
768
+ const matches = text.match(/\b[a-z][a-z0-9_-]*\/[a-z0-9_][a-z0-9_/-]*\b/gi) ?? [];
769
+ const deduped = [];
770
+ const seen = new Set();
771
+ for (const rawMatch of matches) {
772
+ const candidate = rawMatch.trim().replace(/[.,!?;:]+$/g, '');
773
+ try {
774
+ const parsed = (0, entity_resolution_1.parseEntityString)(candidate);
775
+ const normalized = `${parsed.entityType}/${parsed.entityId}`;
776
+ if (seen.has(normalized))
777
+ continue;
778
+ seen.add(normalized);
779
+ deduped.push(normalized);
780
+ }
781
+ catch {
782
+ continue;
783
+ }
784
+ }
785
+ return deduped;
786
+ }
364
787
  function normalizeMessage(message) {
365
788
  return (message ?? '').trim();
366
789
  }
@@ -427,6 +850,134 @@ function normalizeStringArray(value, maxItems = 5, maxLength = 160) {
427
850
  }
428
851
  return out;
429
852
  }
853
+ function normalizeCheckpointFileChanges(value, maxItems = 25) {
854
+ if (!Array.isArray(value))
855
+ return [];
856
+ const out = [];
857
+ for (const item of value) {
858
+ if (!item || typeof item !== 'object')
859
+ continue;
860
+ const record = item;
861
+ const path = typeof record.path === 'string' ? truncate(record.path.trim(), 240) : '';
862
+ if (!path)
863
+ continue;
864
+ const action = typeof record.action === 'string' && record.action.trim()
865
+ ? truncate(record.action.trim().toLowerCase(), 40)
866
+ : 'updated';
867
+ const next = { action, path };
868
+ if (typeof record.toPath === 'string' && record.toPath.trim()) {
869
+ next.toPath = truncate(record.toPath.trim(), 240);
870
+ }
871
+ if (typeof record.purpose === 'string' && record.purpose.trim()) {
872
+ next.purpose = truncate(record.purpose.trim(), 180);
873
+ }
874
+ out.push(next);
875
+ if (out.length >= maxItems)
876
+ break;
877
+ }
878
+ return out;
879
+ }
880
+ function normalizeCheckpointActions(value, maxItems = 25) {
881
+ if (!Array.isArray(value))
882
+ return [];
883
+ const out = [];
884
+ for (const item of value) {
885
+ if (!item || typeof item !== 'object')
886
+ continue;
887
+ const record = item;
888
+ const summary = typeof record.summary === 'string' ? truncate(record.summary.trim(), 220) : '';
889
+ if (!summary)
890
+ continue;
891
+ const kind = typeof record.kind === 'string' && record.kind.trim()
892
+ ? truncate(record.kind.trim().toLowerCase(), 40)
893
+ : 'action';
894
+ const next = { kind, summary };
895
+ if (typeof record.status === 'string' && record.status.trim()) {
896
+ next.status = truncate(record.status.trim().toLowerCase(), 40);
897
+ }
898
+ if (typeof record.target === 'string' && record.target.trim()) {
899
+ next.target = truncate(record.target.trim(), 240);
900
+ }
901
+ if (typeof record.detail === 'string' && record.detail.trim()) {
902
+ next.detail = truncate(record.detail.trim(), 220);
903
+ }
904
+ out.push(next);
905
+ if (out.length >= maxItems)
906
+ break;
907
+ }
908
+ return out;
909
+ }
910
+ function readCheckpointFileChanges(value) {
911
+ if (!value || typeof value !== 'object')
912
+ return [];
913
+ const record = value;
914
+ const items = Array.isArray(record.items) ? record.items : [];
915
+ return items.filter((item) => typeof item === 'object' && item !== null);
916
+ }
917
+ function mergeCheckpointFileChanges(existing, incoming) {
918
+ const seen = new Set();
919
+ const merged = [];
920
+ for (const item of [...readCheckpointFileChanges(existing), ...incoming]) {
921
+ const identity = JSON.stringify(item);
922
+ if (seen.has(identity))
923
+ continue;
924
+ seen.add(identity);
925
+ merged.push(item);
926
+ }
927
+ return { items: merged };
928
+ }
929
+ function readCheckpointActions(value) {
930
+ if (!value || typeof value !== 'object')
931
+ return [];
932
+ const record = value;
933
+ const items = Array.isArray(record.items) ? record.items : [];
934
+ return items.filter((item) => typeof item === 'object' && item !== null);
935
+ }
936
+ function mergeCheckpointActions(existing, incoming) {
937
+ const seen = new Set();
938
+ const merged = [];
939
+ for (const item of [...readCheckpointActions(existing), ...incoming]) {
940
+ const identity = JSON.stringify(item);
941
+ if (seen.has(identity))
942
+ continue;
943
+ seen.add(identity);
944
+ merged.push(item);
945
+ }
946
+ return { items: merged };
947
+ }
948
+ function summarizeCheckpointFileChanges(value) {
949
+ const parts = value.items.map((item) => {
950
+ const action = String(item.action ?? 'updated').trim();
951
+ const path = String(item.path ?? '').trim();
952
+ const toPath = String(item.toPath ?? '').trim();
953
+ const purpose = String(item.purpose ?? '').trim();
954
+ if (!path)
955
+ return '';
956
+ if (toPath) {
957
+ return `${action} ${path} to ${toPath}${purpose ? ` (${purpose})` : ''}`;
958
+ }
959
+ return `${action} ${path}${purpose ? ` (${purpose})` : ''}`;
960
+ }).filter(Boolean);
961
+ return truncate(parts.length > 0
962
+ ? `recent file changes include ${parts.join('; ')}`
963
+ : 'recent file changes were logged.', 220);
964
+ }
965
+ function summarizeCheckpointActions(value) {
966
+ const parts = value.items.map((item) => {
967
+ const kind = String(item.kind ?? 'action').trim();
968
+ const summary = String(item.summary ?? '').trim();
969
+ const status = String(item.status ?? '').trim();
970
+ const target = String(item.target ?? '').trim();
971
+ if (!summary)
972
+ return '';
973
+ const prefix = status ? `[${status}] ` : '';
974
+ const suffix = target ? ` (${target})` : '';
975
+ return `${prefix}${kind}: ${summary}${suffix}`;
976
+ }).filter(Boolean);
977
+ return truncate(parts.length > 0
978
+ ? `recent actions include ${parts.join('; ')}`
979
+ : 'recent actions were logged.', 220);
980
+ }
430
981
  function normalizeCheckpointPayload(payload) {
431
982
  if (typeof payload === 'string') {
432
983
  return {
@@ -452,6 +1003,14 @@ function normalizeCheckpointPayload(payload) {
452
1003
  if (recentOutputs.length > 0) {
453
1004
  normalized.recentOutputs = recentOutputs;
454
1005
  }
1006
+ const actions = normalizeCheckpointActions(raw.actions);
1007
+ if (actions.length > 0) {
1008
+ normalized.actions = actions;
1009
+ }
1010
+ const fileChanges = normalizeCheckpointFileChanges(raw.fileChanges);
1011
+ if (fileChanges.length > 0) {
1012
+ normalized.fileChanges = fileChanges;
1013
+ }
455
1014
  const entityTargets = normalizeStringArray(raw.entityTargets, 5, 180);
456
1015
  if (entityTargets.length > 0) {
457
1016
  normalized.entityTargets = entityTargets;
@@ -461,6 +1020,221 @@ function normalizeCheckpointPayload(payload) {
461
1020
  }
462
1021
  return normalized;
463
1022
  }
1023
+ async function persistSharedCheckpointBreadcrumbs(params) {
1024
+ const { agentId, sessionId, checkpoint } = params;
1025
+ const targets = Array.isArray(checkpoint.entityTargets) ? checkpoint.entityTargets : [];
1026
+ if (targets.length === 0)
1027
+ return [];
1028
+ const expectedKeys = [];
1029
+ const checkpointSummary = {
1030
+ currentStep: checkpoint.currentStep ?? null,
1031
+ nextStep: checkpoint.nextStep ?? null,
1032
+ openRisks: checkpoint.openRisks ?? [],
1033
+ recentOutputs: checkpoint.recentOutputs ?? [],
1034
+ actions: checkpoint.actions ?? [],
1035
+ fileChanges: checkpoint.fileChanges ?? [],
1036
+ notes: checkpoint.notes ?? null,
1037
+ sessionId,
1038
+ };
1039
+ for (const target of targets) {
1040
+ const parsed = (0, entity_resolution_1.parseEntityString)(target);
1041
+ const resolved = await (0, entity_resolution_1.resolveEntity)({
1042
+ entityType: parsed.entityType,
1043
+ entityId: parsed.entityId,
1044
+ rawName: target,
1045
+ aliases: [target],
1046
+ source: 'AttendantCheckpoint',
1047
+ confidence: 95,
1048
+ createIfMissing: true,
1049
+ });
1050
+ const common = {
1051
+ entityType: resolved.entityType,
1052
+ entityId: resolved.entityId,
1053
+ confidence: 95,
1054
+ source: 'AttendantCheckpoint',
1055
+ createdBy: agentId,
1056
+ };
1057
+ const checkpointBaseProperties = {
1058
+ memoryScope: 'project',
1059
+ capturePhase: 'checkpoint',
1060
+ sessionId,
1061
+ };
1062
+ await (0, librarian_1.librarianWrite)({
1063
+ ...common,
1064
+ key: 'checkpoint_summary',
1065
+ valueRaw: checkpointSummary,
1066
+ valueSummary: truncate(`checkpoint summary: current step ${checkpoint.currentStep ?? 'n/a'}; next step ${checkpoint.nextStep ?? 'n/a'}`, 220),
1067
+ properties: {
1068
+ ...checkpointBaseProperties,
1069
+ durableClass: 'checkpoint_summary',
1070
+ canonicalKey: 'checkpoint_summary',
1071
+ mergeStrategy: 'replace',
1072
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
1073
+ memoryScope: 'project',
1074
+ durableClass: 'checkpoint_summary',
1075
+ mergeStrategy: 'replace',
1076
+ extraTags: ['checkpoint', 'breadcrumb'],
1077
+ }),
1078
+ },
1079
+ });
1080
+ expectedKeys.push({
1081
+ entityType: resolved.entityType,
1082
+ entityId: resolved.entityId,
1083
+ key: 'checkpoint_summary',
1084
+ });
1085
+ if (checkpoint.currentStep) {
1086
+ await (0, librarian_1.librarianWrite)({
1087
+ ...common,
1088
+ key: 'current_step',
1089
+ valueRaw: { text: checkpoint.currentStep },
1090
+ valueSummary: truncate(`current step is ${checkpoint.currentStep}`, 220),
1091
+ properties: {
1092
+ ...checkpointBaseProperties,
1093
+ durableClass: 'current_step',
1094
+ canonicalKey: 'current_step',
1095
+ mergeStrategy: 'replace',
1096
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
1097
+ memoryScope: 'project',
1098
+ durableClass: 'current_step',
1099
+ mergeStrategy: 'replace',
1100
+ extraTags: ['checkpoint', 'breadcrumb'],
1101
+ }),
1102
+ },
1103
+ });
1104
+ expectedKeys.push({
1105
+ entityType: resolved.entityType,
1106
+ entityId: resolved.entityId,
1107
+ key: 'current_step',
1108
+ });
1109
+ }
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;
1124
+ await (0, librarian_1.librarianWrite)({
1125
+ ...common,
1126
+ key: 'next_step',
1127
+ valueRaw: { instruction: mergedNextStep },
1128
+ valueSummary: truncate(`next step is ${mergedNextStep}`, 220),
1129
+ properties: {
1130
+ ...checkpointBaseProperties,
1131
+ durableClass: 'next_step',
1132
+ canonicalKey: 'next_step',
1133
+ mergeStrategy: 'replace',
1134
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
1135
+ memoryScope: 'project',
1136
+ durableClass: 'next_step',
1137
+ mergeStrategy: 'replace',
1138
+ extraTags: ['checkpoint', 'breadcrumb'],
1139
+ }),
1140
+ },
1141
+ });
1142
+ expectedKeys.push({
1143
+ entityType: resolved.entityType,
1144
+ entityId: resolved.entityId,
1145
+ key: 'next_step',
1146
+ });
1147
+ }
1148
+ if (Array.isArray(checkpoint.fileChanges) && checkpoint.fileChanges.length > 0) {
1149
+ const existingFileChanges = await (0, queries_2.findEntry)({
1150
+ entityType: resolved.entityType,
1151
+ entityId: resolved.entityId,
1152
+ key: 'recent_file_changes',
1153
+ });
1154
+ const mergedFileChanges = mergeCheckpointFileChanges(existingFileChanges?.valueRaw, checkpoint.fileChanges.map((change) => ({ ...change })));
1155
+ await (0, librarian_1.librarianWrite)({
1156
+ ...common,
1157
+ key: 'recent_file_changes',
1158
+ valueRaw: mergedFileChanges,
1159
+ valueSummary: summarizeCheckpointFileChanges(mergedFileChanges),
1160
+ properties: {
1161
+ ...checkpointBaseProperties,
1162
+ durableClass: 'file_change',
1163
+ canonicalKey: 'recent_file_changes',
1164
+ mergeStrategy: 'append_dedupe',
1165
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
1166
+ memoryScope: 'project',
1167
+ durableClass: 'file_change',
1168
+ mergeStrategy: 'append_dedupe',
1169
+ extraTags: ['checkpoint'],
1170
+ }),
1171
+ },
1172
+ });
1173
+ expectedKeys.push({
1174
+ entityType: resolved.entityType,
1175
+ entityId: resolved.entityId,
1176
+ key: 'recent_file_changes',
1177
+ });
1178
+ }
1179
+ if (Array.isArray(checkpoint.actions) && checkpoint.actions.length > 0) {
1180
+ const existingActions = await (0, queries_2.findEntry)({
1181
+ entityType: resolved.entityType,
1182
+ entityId: resolved.entityId,
1183
+ key: 'recent_actions',
1184
+ });
1185
+ const mergedActions = mergeCheckpointActions(existingActions?.valueRaw, checkpoint.actions.map((action) => ({ ...action })));
1186
+ await (0, librarian_1.librarianWrite)({
1187
+ ...common,
1188
+ key: 'recent_actions',
1189
+ valueRaw: mergedActions,
1190
+ valueSummary: summarizeCheckpointActions(mergedActions),
1191
+ properties: {
1192
+ ...checkpointBaseProperties,
1193
+ durableClass: 'action_log',
1194
+ canonicalKey: 'recent_actions',
1195
+ mergeStrategy: 'append_dedupe',
1196
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
1197
+ memoryScope: 'project',
1198
+ durableClass: 'action_log',
1199
+ mergeStrategy: 'append_dedupe',
1200
+ extraTags: ['checkpoint'],
1201
+ }),
1202
+ },
1203
+ });
1204
+ expectedKeys.push({
1205
+ entityType: resolved.entityType,
1206
+ entityId: resolved.entityId,
1207
+ key: 'recent_actions',
1208
+ });
1209
+ }
1210
+ if (checkpoint.openRisks && checkpoint.openRisks.length > 0) {
1211
+ await (0, librarian_1.librarianWrite)({
1212
+ ...common,
1213
+ key: 'open_risks',
1214
+ valueRaw: { items: checkpoint.openRisks },
1215
+ valueSummary: truncate(`open risks include ${checkpoint.openRisks.join('; ')}`, 220),
1216
+ properties: {
1217
+ ...checkpointBaseProperties,
1218
+ durableClass: 'open_risks',
1219
+ canonicalKey: 'open_risks',
1220
+ mergeStrategy: 'replace',
1221
+ ...(0, semanticFactTags_1.buildSemanticFactTags)({
1222
+ memoryScope: 'project',
1223
+ durableClass: 'open_risks',
1224
+ mergeStrategy: 'replace',
1225
+ extraTags: ['checkpoint', 'breadcrumb'],
1226
+ }),
1227
+ },
1228
+ });
1229
+ expectedKeys.push({
1230
+ entityType: resolved.entityType,
1231
+ entityId: resolved.entityId,
1232
+ key: 'open_risks',
1233
+ });
1234
+ }
1235
+ }
1236
+ return expectedKeys;
1237
+ }
464
1238
  function createSessionId(agentId, taskFingerprint) {
465
1239
  const seed = `${agentId}:${taskFingerprint}:${Date.now()}`;
466
1240
  let hash = 0;
@@ -531,6 +1305,15 @@ function heuristicMemoryNeed(message) {
531
1305
  explanation: 'memory_reference_detected',
532
1306
  };
533
1307
  }
1308
+ // Any substantive message in a project-bound context likely benefits from memory.
1309
+ // Greetings and acks are already caught by MEMORY_NEED_NEGATIVE_PATTERNS above.
1310
+ if (normalized.length > 20) {
1311
+ return {
1312
+ needed: true,
1313
+ confidence: 0.75,
1314
+ explanation: 'substantive_project_prompt',
1315
+ };
1316
+ }
534
1317
  return {
535
1318
  needed: null,
536
1319
  confidence: 0.55,
@@ -541,14 +1324,253 @@ function heuristicMemoryNeed(message) {
541
1324
  class AttendantInstance {
542
1325
  constructor(agentId) {
543
1326
  this.brief = null;
1327
+ this.advisoryLearningProfile = null;
544
1328
  this.contextCallCount = 0;
1329
+ this.attendsWithoutPersist = 0;
1330
+ this.consecutivePreResponseWithoutPost = 0;
1331
+ this.consecutiveUnusedMemoryInjections = 0;
1332
+ this.lastAttendPhase = undefined;
1333
+ this.complianceUpdatedAt = new Date().toISOString();
545
1334
  this.sessionStarted = new Date().toISOString();
546
1335
  this.sessionCheckpoint = null;
1336
+ this.eventSource = 'internal';
1337
+ this.eventHost = null;
1338
+ this.sharedStateObservedAt = null;
1339
+ this.pendingSharedStateInvalidations = new Map();
1340
+ this.pendingMemoryAttributions = [];
547
1341
  this.agentId = agentId;
1342
+ (0, sharedStateInvalidation_1.registerSharedStateInvalidationObserver)(agentId, this);
1343
+ }
1344
+ setLedgerContext(context) {
1345
+ if (context?.source?.trim()) {
1346
+ this.eventSource = context.source.trim();
1347
+ }
1348
+ if (typeof context?.host === 'string') {
1349
+ const trimmed = context.host.trim();
1350
+ this.eventHost = trimmed || null;
1351
+ }
1352
+ else if (context?.host === null) {
1353
+ this.eventHost = null;
1354
+ }
1355
+ }
1356
+ buildEventMetadata(metadata = {}) {
1357
+ const withSession = Object.prototype.hasOwnProperty.call(metadata, 'sessionId')
1358
+ ? metadata
1359
+ : { ...metadata, sessionId: this.sessionStarted };
1360
+ return {
1361
+ ...withSession,
1362
+ ...(this.eventHost ? { host: this.eventHost } : {}),
1363
+ };
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
+ }
1515
+ async loadSessionLedgerSignals(taskType) {
1516
+ try {
1517
+ const source = this.eventSource === 'internal' ? undefined : this.eventSource;
1518
+ const [learnings, profile] = await Promise.all([
1519
+ (0, sessionLedger_1.summarizeSessionLedgerLearnings)({
1520
+ agentId: this.agentId,
1521
+ source,
1522
+ host: this.eventHost ?? undefined,
1523
+ limit: 40,
1524
+ maxLearnings: 4,
1525
+ }),
1526
+ (0, sessionLedger_1.buildSessionLedgerLearningProfile)({
1527
+ agentId: this.agentId,
1528
+ source,
1529
+ host: this.eventHost ?? undefined,
1530
+ taskType,
1531
+ limit: 60,
1532
+ }),
1533
+ ]);
1534
+ return { learnings, profile };
1535
+ }
1536
+ catch (error) {
1537
+ if (error instanceof sessionLedger_1.SessionLedgerUnavailableError) {
1538
+ return { learnings: [], profile: null };
1539
+ }
1540
+ return { learnings: [], profile: null };
1541
+ }
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;
548
1569
  }
549
1570
  // ── Handshake ────────────────────────────────────────────────────────────
550
1571
  async handshake(context) {
551
1572
  const t0 = (0, metrics_1.timeStart)();
1573
+ this.setLedgerContext(context.ledgerContext);
552
1574
  // Try to resume from persisted state first
553
1575
  const persisted = await this.loadPersistedState();
554
1576
  // Load operating rules from Staff Namespace
@@ -556,7 +1578,15 @@ class AttendantInstance {
556
1578
  // Infer task type
557
1579
  const inferredTaskType = await this.inferTask(context);
558
1580
  // Load knowledge — agent entries + related entities
559
- const workingMemory = await this.buildWorkingMemory(inferredTaskType);
1581
+ const [workingMemory, ledgerSignals, projectPolicies] = await Promise.all([
1582
+ this.buildWorkingMemory(inferredTaskType),
1583
+ this.loadSessionLedgerSignals(inferredTaskType),
1584
+ this.loadProjectPolicies(),
1585
+ ]);
1586
+ const sessionLedgerLearnings = ledgerSignals.learnings;
1587
+ this.advisoryLearningProfile = ledgerSignals.profile;
1588
+ const workingMemoryWithPolicies = mergeWorkingMemoryWithProjectPolicies(workingMemory, projectPolicies);
1589
+ const workingMemoryWithLedger = mergeWorkingMemoryWithLedger(workingMemoryWithPolicies, sessionLedgerLearnings);
560
1590
  const recoveryResult = persisted?.sessionCheckpoint
561
1591
  ? this.buildRecovery(context, persisted.sessionCheckpoint)
562
1592
  : { interrupted: false, recovery: null };
@@ -573,29 +1603,40 @@ class AttendantInstance {
573
1603
  }
574
1604
  this.brief = {
575
1605
  agentId: this.agentId,
576
- operatingRules,
1606
+ operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(operatingRules, projectPolicies), this.advisoryLearningProfile),
577
1607
  inferredTaskType,
578
- workingMemory,
1608
+ workingMemory: workingMemoryWithLedger,
1609
+ projectPolicies,
579
1610
  sessionStarted: persisted?.sessionStarted ?? this.sessionStarted,
580
1611
  briefGeneratedAt: new Date().toISOString(),
581
1612
  contextCallCount: this.contextCallCount,
582
- backfillSuggestion: buildBackfillSuggestion(context, workingMemory),
1613
+ backfillSuggestion: buildBackfillSuggestion(context, workingMemoryWithLedger),
1614
+ sessionLedgerLearnings,
583
1615
  sessionCheckpoint: this.sessionCheckpoint,
584
1616
  sessionRecovery: recoveryResult.recovery,
1617
+ compliance: persisted?.compliance ?? this.buildComplianceState(),
1618
+ watchedEntities: normalizeWatchedEntities([
1619
+ ...(persisted?.watchedEntities ?? []),
1620
+ ...(this.sessionCheckpoint?.checkpoint.entityTargets ?? []),
1621
+ ...inferContextWatchedEntities(context.task, context.recentMessages),
1622
+ ]),
585
1623
  };
1624
+ this.sharedStateObservedAt = this.brief.briefGeneratedAt;
586
1625
  await this.persistState();
587
1626
  (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
588
1627
  staffComponent: 'Attendant',
589
1628
  actionType: 'handshake_completed',
590
1629
  agentId: this.agentId,
591
- source: 'internal', // Source not threaded to AttendantInstance in this PR; follow-up required
592
- reason: null,
593
- level: 'debug',
594
- metadata: {
1630
+ source: this.eventSource,
1631
+ reason: 'session_started',
1632
+ level: 'audit',
1633
+ metadata: this.buildEventMetadata({
595
1634
  briefSize: this.brief?.workingMemory.length ?? 0,
1635
+ ledgerLearningCount: sessionLedgerLearnings.length,
1636
+ projectPolicyCount: projectPolicies.length,
1637
+ advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
596
1638
  taskSummary: context.task.slice(0, 120),
597
- sessionId: this.sessionStarted,
598
- },
1639
+ }),
599
1640
  });
600
1641
  (0, metrics_1.timeEnd)('attendant.handshake_ms', t0);
601
1642
  return this.brief;
@@ -603,12 +1644,18 @@ class AttendantInstance {
603
1644
  // ── Reconvene ────────────────────────────────────────────────────────────
604
1645
  async reconvene(context) {
605
1646
  const t0 = (0, metrics_1.timeStart)();
1647
+ this.setLedgerContext(context.ledgerContext);
606
1648
  if (!this.brief) {
607
1649
  const result = await this.handshake(context);
608
1650
  (0, metrics_1.timeEnd)('attendant.reconvene_ms', t0);
609
1651
  return result;
610
1652
  }
611
1653
  const newTaskType = await this.inferTask(context);
1654
+ const [ledgerSignals, projectPolicies] = await Promise.all([
1655
+ this.loadSessionLedgerSignals(newTaskType),
1656
+ this.loadProjectPolicies(),
1657
+ ]);
1658
+ this.advisoryLearningProfile = ledgerSignals.profile;
612
1659
  // Task hasn't shifted — update timestamp only
613
1660
  if (newTaskType.toLowerCase() === this.brief.inferredTaskType.toLowerCase()) {
614
1661
  if (this.sessionCheckpoint && this.sessionCheckpoint.status === 'active') {
@@ -621,24 +1668,31 @@ class AttendantInstance {
621
1668
  }
622
1669
  this.brief = {
623
1670
  ...this.brief,
1671
+ operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(await this.loadOperatingRules(), projectPolicies), this.advisoryLearningProfile),
1672
+ workingMemory: mergeWorkingMemoryWithLedger(mergeWorkingMemoryWithProjectPolicies(this.brief.workingMemory, projectPolicies), ledgerSignals.learnings),
1673
+ projectPolicies,
624
1674
  briefGeneratedAt: new Date().toISOString(),
625
1675
  contextCallCount: this.contextCallCount,
1676
+ sessionLedgerLearnings: ledgerSignals.learnings,
626
1677
  sessionCheckpoint: this.sessionCheckpoint,
627
1678
  sessionRecovery: null,
1679
+ compliance: this.buildComplianceState(),
628
1680
  };
1681
+ this.sharedStateObservedAt = this.brief.briefGeneratedAt;
629
1682
  await this.persistState();
630
1683
  (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
631
1684
  staffComponent: 'Attendant',
632
1685
  actionType: 'reconvene_completed',
633
1686
  agentId: this.agentId,
634
- source: 'internal',
635
- reason: 'Task unchanged brief timestamp refreshed.',
1687
+ source: this.eventSource,
1688
+ reason: 'Task unchanged - brief timestamp refreshed.',
636
1689
  level: 'audit',
637
- metadata: {
1690
+ metadata: this.buildEventMetadata({
638
1691
  briefSize: this.brief?.workingMemory.length ?? 0,
639
- sessionId: this.sessionStarted,
640
1692
  contextCallCount: this.contextCallCount,
641
- },
1693
+ projectPolicyCount: projectPolicies.length,
1694
+ advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
1695
+ }),
642
1696
  });
643
1697
  (0, metrics_1.timeEnd)('attendant.reconvene_ms', t0);
644
1698
  return this.brief;
@@ -647,26 +1701,32 @@ class AttendantInstance {
647
1701
  const workingMemory = await this.buildWorkingMemory(newTaskType);
648
1702
  this.brief = {
649
1703
  ...this.brief,
1704
+ operatingRules: applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(await this.loadOperatingRules(), projectPolicies), this.advisoryLearningProfile),
650
1705
  inferredTaskType: newTaskType,
651
- workingMemory,
1706
+ workingMemory: mergeWorkingMemoryWithLedger(mergeWorkingMemoryWithProjectPolicies(workingMemory, projectPolicies), ledgerSignals.learnings),
1707
+ projectPolicies,
652
1708
  briefGeneratedAt: new Date().toISOString(),
653
1709
  contextCallCount: this.contextCallCount,
1710
+ sessionLedgerLearnings: ledgerSignals.learnings,
654
1711
  sessionCheckpoint: this.sessionCheckpoint,
655
1712
  sessionRecovery: null,
1713
+ compliance: this.buildComplianceState(),
656
1714
  };
1715
+ this.sharedStateObservedAt = this.brief.briefGeneratedAt;
657
1716
  await this.persistState();
658
1717
  (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
659
1718
  staffComponent: 'Attendant',
660
1719
  actionType: 'reconvene_completed',
661
1720
  agentId: this.agentId,
662
- source: 'internal',
663
- reason: 'Task shifted working memory rebuilt.',
1721
+ source: this.eventSource,
1722
+ reason: 'Task shifted - working memory rebuilt.',
664
1723
  level: 'audit',
665
- metadata: {
1724
+ metadata: this.buildEventMetadata({
666
1725
  briefSize: this.brief?.workingMemory.length ?? 0,
667
- sessionId: this.sessionStarted,
668
1726
  contextCallCount: this.contextCallCount,
669
- },
1727
+ projectPolicyCount: projectPolicies.length,
1728
+ advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
1729
+ }),
670
1730
  });
671
1731
  (0, metrics_1.timeEnd)('attendant.reconvene_ms', t0);
672
1732
  return this.brief;
@@ -692,8 +1752,10 @@ class AttendantInstance {
692
1752
  const operatingRules = rulesResult.found && rulesResult.entry
693
1753
  ? formatOperatingRulesText(rulesResult.entry.valueRaw, rulesResult.entry.valueSummary)
694
1754
  : formatOperatingRulesText(null, 'Attendant operating rules:');
1755
+ const projectPolicies = this.brief?.projectPolicies ?? await this.loadProjectPolicies();
695
1756
  if (this.brief) {
696
- this.brief.operatingRules = operatingRules;
1757
+ this.brief.operatingRules = applyAdvisoryOperatingRules(applyProjectPolicyOperatingRules(operatingRules, projectPolicies), this.advisoryLearningProfile);
1758
+ this.brief.projectPolicies = projectPolicies;
697
1759
  this.brief.contextCallCount = 0;
698
1760
  }
699
1761
  this.contextCallCount = 0;
@@ -702,26 +1764,69 @@ class AttendantInstance {
702
1764
  staffComponent: 'Attendant',
703
1765
  actionType: 'session_expired',
704
1766
  agentId: this.agentId,
705
- source: 'internal',
1767
+ source: this.eventSource,
706
1768
  reason: 'Context window threshold reached. Session archived.',
707
1769
  level: 'audit',
708
- metadata: {
709
- sessionId: this.sessionStarted,
1770
+ metadata: this.buildEventMetadata({
710
1771
  contextCallCount: 0,
711
1772
  expiryReason: 'context_low',
712
- },
1773
+ }),
713
1774
  });
714
1775
  }
715
1776
  // ── Getters ──────────────────────────────────────────────────────────────
716
1777
  getBrief() {
717
1778
  return this.brief;
718
1779
  }
1780
+ async verifyCheckpointAvailability(sessionId, expectedKeys) {
1781
+ const persisted = await readPersistedBriefForAgent(this.agentId);
1782
+ if (!persisted?.sessionCheckpoint || persisted.sessionCheckpoint.sessionId !== sessionId) {
1783
+ throw new Error(`CHECKPOINT_AVAILABILITY_FAILED: agent/${this.agentId}/attendant_state was not immediately queryable with session ${sessionId}.`);
1784
+ }
1785
+ for (const expected of expectedKeys) {
1786
+ const observed = await (0, queries_2.findEntry)(expected);
1787
+ if (!observed) {
1788
+ throw new Error(`CHECKPOINT_AVAILABILITY_FAILED: ${expected.entityType}/${expected.entityId}/${expected.key} was not immediately queryable after checkpoint success.`);
1789
+ }
1790
+ }
1791
+ }
1792
+ buildComplianceState(lastUpdated) {
1793
+ return buildSessionComplianceState({
1794
+ attendsWithoutPersist: this.attendsWithoutPersist,
1795
+ consecutivePreResponseWithoutPost: this.consecutivePreResponseWithoutPost,
1796
+ consecutiveUnusedMemoryInjections: this.consecutiveUnusedMemoryInjections,
1797
+ lastAttendPhase: this.lastAttendPhase,
1798
+ lastUpdated: lastUpdated ?? this.complianceUpdatedAt,
1799
+ });
1800
+ }
1801
+ async notifyWriteOccurred() {
1802
+ this.attendsWithoutPersist = 0;
1803
+ this.lastAttendPhase = undefined;
1804
+ this.consecutivePreResponseWithoutPost = 0;
1805
+ this.complianceUpdatedAt = new Date().toISOString();
1806
+ this.recordMemoryEvidence('write');
1807
+ if (!this.brief) {
1808
+ return;
1809
+ }
1810
+ this.brief = {
1811
+ ...this.brief,
1812
+ compliance: this.buildComplianceState(this.complianceUpdatedAt),
1813
+ briefGeneratedAt: this.complianceUpdatedAt,
1814
+ };
1815
+ await this.persistState();
1816
+ }
719
1817
  async checkpoint(input) {
1818
+ this.attendsWithoutPersist = 0;
1819
+ this.lastAttendPhase = undefined;
1820
+ this.consecutivePreResponseWithoutPost = 0;
1821
+ this.complianceUpdatedAt = new Date().toISOString();
1822
+ this.recordMemoryEvidence('checkpoint');
1823
+ this.setLedgerContext(input.ledgerContext);
720
1824
  const now = new Date().toISOString();
721
1825
  if (!this.brief) {
722
1826
  await this.handshake({
723
1827
  task: input.task,
724
1828
  recentMessages: input.recentMessages,
1829
+ ledgerContext: input.ledgerContext,
725
1830
  });
726
1831
  }
727
1832
  const normalizedCheckpoint = normalizeCheckpointPayload(input.checkpoint);
@@ -746,15 +1851,88 @@ class AttendantInstance {
746
1851
  ...this.brief,
747
1852
  sessionCheckpoint: this.sessionCheckpoint,
748
1853
  sessionRecovery: null,
1854
+ compliance: this.buildComplianceState(now),
749
1855
  briefGeneratedAt: now,
1856
+ watchedEntities: normalizeWatchedEntities([
1857
+ ...(this.brief.watchedEntities ?? []),
1858
+ ...(normalizedCheckpoint.entityTargets ?? []),
1859
+ ]),
750
1860
  };
1861
+ this.sharedStateObservedAt = now;
1862
+ let expectedSharedKeys = [];
1863
+ try {
1864
+ expectedSharedKeys = await persistSharedCheckpointBreadcrumbs({
1865
+ agentId: this.agentId,
1866
+ sessionId,
1867
+ checkpoint: normalizedCheckpoint,
1868
+ });
1869
+ }
1870
+ catch (error) {
1871
+ (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
1872
+ staffComponent: 'Attendant',
1873
+ actionType: 'checkpoint_shared_breadcrumb_failed',
1874
+ agentId: this.agentId,
1875
+ source: this.eventSource,
1876
+ reason: 'shared_checkpoint_breadcrumb_failed',
1877
+ level: 'audit',
1878
+ metadata: this.buildEventMetadata({
1879
+ sessionId,
1880
+ error: error instanceof Error ? error.message : String(error),
1881
+ }),
1882
+ });
1883
+ throw error;
1884
+ }
751
1885
  await this.persistState();
1886
+ try {
1887
+ await this.verifyCheckpointAvailability(sessionId, expectedSharedKeys);
1888
+ }
1889
+ catch (error) {
1890
+ (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
1891
+ staffComponent: 'Attendant',
1892
+ actionType: 'checkpoint_availability_failed',
1893
+ agentId: this.agentId,
1894
+ source: this.eventSource,
1895
+ reason: 'checkpoint_not_immediately_queryable',
1896
+ level: 'audit',
1897
+ metadata: this.buildEventMetadata({
1898
+ sessionId,
1899
+ sharedKeyCount: expectedSharedKeys.length,
1900
+ error: error instanceof Error ? error.message : String(error),
1901
+ }),
1902
+ });
1903
+ throw error;
1904
+ }
1905
+ (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
1906
+ staffComponent: 'Attendant',
1907
+ actionType: 'checkpoint_written',
1908
+ agentId: this.agentId,
1909
+ source: this.eventSource,
1910
+ reason: 'shared_checkpoint_written',
1911
+ level: 'audit',
1912
+ metadata: this.buildEventMetadata({
1913
+ sessionId,
1914
+ currentStep: normalizedCheckpoint.currentStep ?? null,
1915
+ nextStep: normalizedCheckpoint.nextStep ?? null,
1916
+ openRiskCount: normalizedCheckpoint.openRisks?.length ?? 0,
1917
+ recentOutputCount: normalizedCheckpoint.recentOutputs?.length ?? 0,
1918
+ actionCount: normalizedCheckpoint.actions?.length ?? 0,
1919
+ fileChangeCount: normalizedCheckpoint.fileChanges?.length ?? 0,
1920
+ entityTargetCount: normalizedCheckpoint.entityTargets?.length ?? 0,
1921
+ sharedKeyCount: expectedSharedKeys.length,
1922
+ availabilityVerified: true,
1923
+ }),
1924
+ });
752
1925
  return this.brief;
753
1926
  }
754
1927
  async resumeSession(input = {}) {
1928
+ this.setLedgerContext(input.ledgerContext);
755
1929
  await this.ensureSessionLoaded();
756
1930
  if (!this.brief || !this.sessionCheckpoint) {
757
- return this.brief ?? (await this.handshake({ task: 'resume session', recentMessages: [] }));
1931
+ return this.brief ?? (await this.handshake({
1932
+ task: 'resume session',
1933
+ recentMessages: [],
1934
+ ledgerContext: input.ledgerContext,
1935
+ }));
758
1936
  }
759
1937
  if (input.sessionId && input.sessionId.trim() !== this.sessionCheckpoint.sessionId) {
760
1938
  throw new Error(`Session "${input.sessionId}" does not match the active checkpoint.`);
@@ -771,15 +1949,22 @@ class AttendantInstance {
771
1949
  ...this.brief,
772
1950
  sessionCheckpoint: this.sessionCheckpoint,
773
1951
  sessionRecovery: null,
1952
+ compliance: this.buildComplianceState(now),
774
1953
  briefGeneratedAt: now,
775
1954
  };
1955
+ this.sharedStateObservedAt = now;
776
1956
  await this.persistState();
777
1957
  return this.brief;
778
1958
  }
779
1959
  async completeSession(input = {}) {
1960
+ this.setLedgerContext(input.ledgerContext);
780
1961
  await this.ensureSessionLoaded();
781
1962
  if (!this.brief || !this.sessionCheckpoint) {
782
- return this.brief ?? (await this.handshake({ task: 'complete session', recentMessages: [] }));
1963
+ return this.brief ?? (await this.handshake({
1964
+ task: 'complete session',
1965
+ recentMessages: [],
1966
+ ledgerContext: input.ledgerContext,
1967
+ }));
783
1968
  }
784
1969
  if (input.sessionId && input.sessionId.trim() !== this.sessionCheckpoint.sessionId) {
785
1970
  throw new Error(`Session "${input.sessionId}" does not match the active checkpoint.`);
@@ -796,15 +1981,22 @@ class AttendantInstance {
796
1981
  ...this.brief,
797
1982
  sessionCheckpoint: this.sessionCheckpoint,
798
1983
  sessionRecovery: null,
1984
+ compliance: this.buildComplianceState(now),
799
1985
  briefGeneratedAt: now,
800
1986
  };
1987
+ this.sharedStateObservedAt = now;
801
1988
  await this.persistState();
802
1989
  return this.brief;
803
1990
  }
804
1991
  async abandonSession(input = {}) {
1992
+ this.setLedgerContext(input.ledgerContext);
805
1993
  await this.ensureSessionLoaded();
806
1994
  if (!this.brief || !this.sessionCheckpoint) {
807
- return this.brief ?? (await this.handshake({ task: 'abandon session', recentMessages: [] }));
1995
+ return this.brief ?? (await this.handshake({
1996
+ task: 'abandon session',
1997
+ recentMessages: [],
1998
+ ledgerContext: input.ledgerContext,
1999
+ }));
808
2000
  }
809
2001
  if (input.sessionId && input.sessionId.trim() !== this.sessionCheckpoint.sessionId) {
810
2002
  throw new Error(`Session "${input.sessionId}" does not match the active checkpoint.`);
@@ -821,14 +2013,18 @@ class AttendantInstance {
821
2013
  ...this.brief,
822
2014
  sessionCheckpoint: this.sessionCheckpoint,
823
2015
  sessionRecovery: null,
2016
+ compliance: this.buildComplianceState(now),
824
2017
  briefGeneratedAt: now,
825
2018
  };
2019
+ this.sharedStateObservedAt = now;
826
2020
  await this.persistState();
827
2021
  return this.brief;
828
2022
  }
829
2023
  async inspectSession(context) {
2024
+ this.setLedgerContext(context?.ledgerContext);
830
2025
  const persisted = await this.loadPersistedState();
831
2026
  const checkpoint = persisted?.sessionCheckpoint ?? this.sessionCheckpoint ?? null;
2027
+ const compliance = persisted?.compliance ?? this.buildComplianceState();
832
2028
  const normalizedTask = typeof context?.task === 'string' ? context.task.trim() : '';
833
2029
  const recentMessages = Array.isArray(context?.recentMessages) ? context.recentMessages : [];
834
2030
  const recovery = checkpoint && normalizedTask
@@ -840,7 +2036,8 @@ class AttendantInstance {
840
2036
  sessionCheckpoint: checkpoint,
841
2037
  sessionRecovery: recovery,
842
2038
  persistedBriefGeneratedAt: persisted?.briefGeneratedAt,
843
- summary: summarizeSessionState(this.agentId, checkpoint, persisted?.briefGeneratedAt),
2039
+ summary: summarizeSessionState(this.agentId, checkpoint, persisted?.briefGeneratedAt, compliance),
2040
+ compliance,
844
2041
  };
845
2042
  }
846
2043
  getAgentId() {
@@ -848,53 +2045,183 @@ class AttendantInstance {
848
2045
  }
849
2046
  async attend(input) {
850
2047
  const t0 = (0, metrics_1.timeStart)();
2048
+ this.setLedgerContext(input.ledgerContext);
851
2049
  const currentContext = input.currentContext ?? '';
852
2050
  const latestMessage = normalizeMessage(input.latestMessage);
853
2051
  const forceInject = input.forceInject === true;
854
2052
  let bootstrap = null;
2053
+ this.attendsWithoutPersist++;
2054
+ const phase = input.phase;
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.';
2057
+ if (phase === 'post-response') {
2058
+ // Correct post-response call — reset counters
2059
+ this.attendsWithoutPersist = 0;
2060
+ this.lastAttendPhase = 'post-response';
2061
+ this.consecutivePreResponseWithoutPost = 0;
2062
+ }
2063
+ else if (phase === 'pre-response') {
2064
+ if (this.lastAttendPhase === 'pre-response') {
2065
+ this.consecutivePreResponseWithoutPost++;
2066
+ complianceWarning = `COMPLIANCE: iranti_attend(phase='pre-response') was called without a preceding phase='post-response'. The previous response was delivered without the required post-response attend. After every response, call iranti_attend(phase='post-response') then persist durable findings with iranti_write or iranti_checkpoint.`;
2067
+ }
2068
+ else {
2069
+ this.consecutivePreResponseWithoutPost = 0;
2070
+ }
2071
+ this.lastAttendPhase = 'pre-response';
2072
+ }
2073
+ else {
2074
+ // No phase provided — fall back to heuristic counter
2075
+ if (this.attendsWithoutPersist >= 3) {
2076
+ complianceWarning = `COMPLIANCE: iranti_attend has been called ${this.attendsWithoutPersist} times since the last iranti_write or iranti_checkpoint. You are likely missing post-response attend calls and durable writes. Call iranti_attend(phase='post-response') after every response, then persist durable findings with iranti_write or iranti_checkpoint before the next turn.`;
2077
+ }
2078
+ }
2079
+ this.complianceUpdatedAt = new Date().toISOString();
2080
+ let compliance = this.buildComplianceState(this.complianceUpdatedAt);
2081
+ if (!complianceWarning && compliance.issues.some((issue) => issue.code === 'ignored_injected_memory')) {
2082
+ complianceWarning = ignoredMemoryWarning;
2083
+ }
855
2084
  if (!this.brief) {
856
2085
  const bootstrapTask = buildAttendBootstrapTask(latestMessage, currentContext);
857
- await this.handshake({
2086
+ const bootstrapBrief = await this.handshake({
858
2087
  task: bootstrapTask,
859
2088
  recentMessages: buildAttendBootstrapMessages(latestMessage, currentContext),
2089
+ ledgerContext: input.ledgerContext,
860
2090
  });
861
2091
  bootstrap = {
862
2092
  handshakePerformed: true,
863
2093
  reason: 'no_existing_brief',
864
2094
  task: bootstrapTask,
2095
+ operatingRules: bootstrapBrief.operatingRules,
2096
+ note: 'A handshake was auto-performed because no session brief existed. Read operatingRules now and follow any instructions there — including ACKNOWLEDGE — before replying to the user.',
865
2097
  };
866
2098
  }
867
2099
  const effectiveEntityHints = this.resolveAttendEntityHints(input.entityHints, latestMessage);
2100
+ let watchedEntitiesChanged = this.updateWatchedEntities(effectiveEntityHints);
2101
+ const freshState = await this.detectRelevantFreshState(effectiveEntityHints, latestMessage);
868
2102
  const observationContext = currentContext.trim().length > 0 ? currentContext : latestMessage;
869
2103
  const mandatoryRecall = (0, autoRemember_1.detectMandatoryRecall)(latestMessage);
870
- const decision = await this.decideMemoryNeed({
2104
+ if (mandatoryRecall.required && input.suppressEvents !== true) {
2105
+ (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
2106
+ staffComponent: 'Attendant',
2107
+ actionType: 'mandatory_recall_forced',
2108
+ agentId: this.agentId,
2109
+ source: this.eventSource,
2110
+ reason: mandatoryRecall.reason ?? 'mandatory_recall_prompt',
2111
+ level: 'audit',
2112
+ metadata: this.buildEventMetadata({
2113
+ key: mandatoryRecall.key ?? null,
2114
+ latestMessage: latestMessage.slice(0, 160),
2115
+ }),
2116
+ });
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
+ }
2164
+ let decision = await this.decideMemoryNeed({
871
2165
  currentContext,
872
2166
  latestMessage,
873
2167
  forceInject,
2168
+ entityHintCount: effectiveEntityHints.length,
874
2169
  });
2170
+ if (freshState.hasFreshState && !decision.needed) {
2171
+ decision = {
2172
+ needed: true,
2173
+ confidence: 0.92,
2174
+ method: 'heuristic',
2175
+ explanation: 'relevant_shared_state_changed',
2176
+ };
2177
+ }
875
2178
  if (!decision.needed) {
876
2179
  if (input.suppressEvents !== true) {
877
2180
  (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
878
2181
  staffComponent: 'Attendant',
879
2182
  actionType: 'attend_completed',
880
2183
  agentId: this.agentId,
881
- source: 'internal',
882
- reason: null,
883
- level: 'debug',
884
- metadata: {
2184
+ source: this.eventSource,
2185
+ reason: 'memory_not_injected',
2186
+ level: 'audit',
2187
+ metadata: this.buildEventMetadata({
885
2188
  contextCallCount: this.contextCallCount,
886
- sessionId: this.sessionStarted,
887
2189
  shouldInject: false,
888
2190
  attendReason: 'memory_not_needed',
889
- },
2191
+ }),
2192
+ });
2193
+ (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
2194
+ staffComponent: 'Attendant',
2195
+ actionType: 'memory_not_injected',
2196
+ agentId: this.agentId,
2197
+ source: this.eventSource,
2198
+ reason: 'memory_not_needed',
2199
+ level: 'audit',
2200
+ metadata: this.buildEventMetadata({
2201
+ shouldInject: false,
2202
+ factCount: 0,
2203
+ injectedKeys: [],
2204
+ }),
890
2205
  });
891
2206
  }
2207
+ if (this.brief) {
2208
+ this.brief = {
2209
+ ...this.brief,
2210
+ compliance,
2211
+ briefGeneratedAt: this.complianceUpdatedAt,
2212
+ };
2213
+ await this.persistState();
2214
+ }
892
2215
  (0, metrics_1.timeEnd)('attendant.attend_ms', t0);
893
2216
  return {
894
2217
  shouldInject: false,
895
2218
  reason: 'memory_not_needed',
896
2219
  decision,
897
2220
  bootstrap,
2221
+ complianceWarning,
2222
+ compliance,
2223
+ memoryAttributions: [],
2224
+ usageGuidance: buildUsageGuidance('attend'),
898
2225
  facts: [],
899
2226
  entitiesDetected: [],
900
2227
  alreadyPresent: 0,
@@ -912,55 +2239,145 @@ class AttendantInstance {
912
2239
  },
913
2240
  };
914
2241
  }
2242
+ const observeEntityHints = effectiveEntityHints.length > 0 ? effectiveEntityHints : freshState.entities;
915
2243
  const observed = await this.observe({
916
2244
  currentContext: observationContext,
917
2245
  maxFacts: input.maxFacts,
918
- entityHints: effectiveEntityHints,
919
- priorityKeys: mandatoryRecall.key ? [mandatoryRecall.key] : [],
2246
+ entityHints: observeEntityHints,
2247
+ priorityKeys: expandContinuityPriorityKeys(Array.from(new Set([
2248
+ ...(mandatoryRecall.key ? [mandatoryRecall.key] : []),
2249
+ ...(this.advisoryLearningProfile?.priorityKeys ?? []),
2250
+ ...freshState.priorityKeys,
2251
+ ]))),
2252
+ skipContextFilter: forceInject,
2253
+ ledgerContext: input.ledgerContext,
920
2254
  });
2255
+ // Remap facts from personal recall fallback entities to the canonical personal entity.
2256
+ // E.g. if person/user is a legacy alias for user/main, surface facts as user/main/<key>.
2257
+ const canonicalPersonalEntity = (0, autoRemember_1.getPersonalMemoryEntity)();
2258
+ const personalFallbacks = new Set((0, autoRemember_1.getPersonalRecallEntities)().filter((e) => e !== canonicalPersonalEntity));
2259
+ const [canonicalPersonalType, canonicalPersonalId] = canonicalPersonalEntity.split('/', 2);
2260
+ const remappedFacts = personalFallbacks.size === 0 ? observed.facts : observed.facts.map((fact) => {
2261
+ const slashIdx2 = fact.entityKey.indexOf('/', fact.entityKey.indexOf('/') + 1);
2262
+ const entityPath = slashIdx2 === -1 ? fact.entityKey : fact.entityKey.slice(0, slashIdx2);
2263
+ if (!personalFallbacks.has(entityPath))
2264
+ return fact;
2265
+ const remainder = slashIdx2 === -1 ? '' : fact.entityKey.slice(slashIdx2);
2266
+ return { ...fact, entityKey: `${canonicalPersonalType}/${canonicalPersonalId}${remainder}` };
2267
+ });
2268
+ const structuredFacts = (0, hostMemoryFormatting_1.assignStructuredFactIds)(remappedFacts);
2269
+ watchedEntitiesChanged = this.updateWatchedEntities(observed.entitiesResolved?.map((entry) => entry.canonicalEntity) ?? []) || watchedEntitiesChanged;
2270
+ this.markSharedStateObserved(observeEntityHints.length > 0 ? observeEntityHints : freshState.entities);
921
2271
  let reason = 'memory_needed_injected';
922
- const shouldInject = observed.facts.length > 0;
2272
+ const shouldInject = structuredFacts.length > 0;
2273
+ let searchSuggestion;
923
2274
  if (!shouldInject) {
924
2275
  const allAlreadyInContext = observed.totalFound > 0 && observed.alreadyPresent >= observed.totalFound;
925
2276
  reason = allAlreadyInContext ? 'memory_needed_but_in_context' : 'memory_needed_no_facts';
2277
+ if (reason === 'memory_needed_no_facts') {
2278
+ const terms = tokenize(latestMessage).slice(0, 6);
2279
+ const alternativeEntities = (observed.entitiesResolved ?? [])
2280
+ .map((e) => e.canonicalEntity)
2281
+ .filter(Boolean);
2282
+ searchSuggestion = {
2283
+ hint: terms.length > 0
2284
+ ? `No facts found via attend. Try iranti_search with: ${terms.join(' ')}`
2285
+ : 'No facts found via attend. Try iranti_search with a different query or entity path.',
2286
+ suggestedTerms: terms,
2287
+ alternativeEntities,
2288
+ };
2289
+ }
926
2290
  }
927
2291
  else if (forceInject) {
928
2292
  reason = 'forced';
929
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
+ : [];
930
2305
  const attendResult = {
931
2306
  ...observed,
932
- shouldInject,
2307
+ facts: structuredFacts,
2308
+ shouldInject: structuredFacts.length > 0,
933
2309
  reason,
934
2310
  decision,
935
2311
  bootstrap,
2312
+ searchSuggestion,
2313
+ complianceWarning,
2314
+ compliance,
2315
+ memoryAttributions,
2316
+ usageGuidance: buildUsageGuidance('attend'),
936
2317
  };
937
2318
  if (input.suppressEvents !== true) {
938
2319
  (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
939
2320
  staffComponent: 'Attendant',
940
2321
  actionType: 'attend_completed',
941
2322
  agentId: this.agentId,
942
- source: 'internal',
943
- reason: null,
944
- level: 'debug',
945
- metadata: {
2323
+ source: this.eventSource,
2324
+ reason: shouldInject ? 'memory_injected' : 'memory_not_injected',
2325
+ level: 'audit',
2326
+ metadata: this.buildEventMetadata({
946
2327
  contextCallCount: this.contextCallCount,
947
- sessionId: this.sessionStarted,
948
2328
  shouldInject,
949
2329
  attendReason: reason,
950
- },
2330
+ injectionId: memoryAttributions[0]?.injectionId ?? null,
2331
+ advisoryDecisionMethod: decision.method,
2332
+ advisoryScopes: this.advisoryLearningProfile?.scopesUsed ?? [],
2333
+ freshStateEntities: freshState.entities,
2334
+ freshStateKeys: freshState.priorityKeys,
2335
+ }),
2336
+ });
2337
+ (0, staffEventRegistry_1.getStaffEventEmitter)().emit({
2338
+ staffComponent: 'Attendant',
2339
+ actionType: shouldInject ? 'memory_injected' : 'memory_not_injected',
2340
+ agentId: this.agentId,
2341
+ source: this.eventSource,
2342
+ reason,
2343
+ level: 'audit',
2344
+ metadata: this.buildEventMetadata({
2345
+ shouldInject,
2346
+ factCount: observed.facts.length,
2347
+ injectionId: memoryAttributions[0]?.injectionId ?? null,
2348
+ injectedKeys: observed.facts.map((fact) => fact.entityKey),
2349
+ injectedEntryIds: observed.facts
2350
+ .map((fact) => fact.knowledgeEntryId)
2351
+ .filter((value) => typeof value === 'number'),
2352
+ entitiesResolved: observed.entitiesResolved?.map((entry) => entry.canonicalEntity) ?? [],
2353
+ alreadyPresent: observed.alreadyPresent,
2354
+ totalFound: observed.totalFound,
2355
+ advisoryPriorityKeys: this.advisoryLearningProfile?.priorityKeys ?? [],
2356
+ freshStateEntities: freshState.entities,
2357
+ freshStateKeys: freshState.priorityKeys,
2358
+ }),
951
2359
  });
952
2360
  }
2361
+ if (this.brief) {
2362
+ this.brief = {
2363
+ ...this.brief,
2364
+ compliance,
2365
+ briefGeneratedAt: this.complianceUpdatedAt,
2366
+ };
2367
+ }
2368
+ if (watchedEntitiesChanged || this.brief?.compliance !== compliance || memoryAttributions.length > 0) {
2369
+ await this.persistState();
2370
+ }
953
2371
  (0, metrics_1.timeEnd)('attendant.attend_ms', t0);
954
2372
  return attendResult;
955
2373
  }
956
2374
  // Context Window Observation
957
2375
  async observe(input) {
958
2376
  const t0 = (0, metrics_1.timeStart)();
2377
+ this.setLedgerContext(input.ledgerContext);
959
2378
  const maxFacts = input.maxFacts ?? 5;
960
2379
  const currentContext = input.currentContext ?? '';
961
- const entityHints = Array.isArray(input.entityHints)
962
- ? input.entityHints.filter((hint) => typeof hint === 'string' && hint.trim().length > 0)
963
- : [];
2380
+ const entityHints = this.resolveObserveEntityHints(input.entityHints, currentContext);
964
2381
  const requestedPriorityKeys = Array.isArray(input.priorityKeys)
965
2382
  ? input.priorityKeys
966
2383
  .filter((key) => typeof key === 'string' && key.trim().length > 0)
@@ -971,13 +2388,12 @@ class AttendantInstance {
971
2388
  staffComponent: 'Attendant',
972
2389
  actionType: 'observe_completed',
973
2390
  agentId: this.agentId,
974
- source: 'internal',
975
- reason: null,
976
- level: 'debug',
977
- metadata: {
2391
+ source: this.eventSource,
2392
+ reason: 'no_observation_context',
2393
+ level: 'audit',
2394
+ metadata: this.buildEventMetadata({
978
2395
  observeType: 'empty_context',
979
- sessionId: this.sessionStarted,
980
- },
2396
+ }),
981
2397
  });
982
2398
  (0, metrics_1.timeEnd)('attendant.observe_ms', t0);
983
2399
  return {
@@ -985,6 +2401,7 @@ class AttendantInstance {
985
2401
  entitiesDetected: [],
986
2402
  alreadyPresent: 0,
987
2403
  totalFound: 0,
2404
+ usageGuidance: buildUsageGuidance('observe'),
988
2405
  entitiesResolved: [],
989
2406
  debug: {
990
2407
  skipped: 'empty_context',
@@ -1120,13 +2537,12 @@ ${detectionWindow}`,
1120
2537
  staffComponent: 'Attendant',
1121
2538
  actionType: 'observe_completed',
1122
2539
  agentId: this.agentId,
1123
- source: 'internal',
1124
- reason: null,
1125
- level: 'debug',
1126
- metadata: {
2540
+ source: this.eventSource,
2541
+ reason: 'no_entity_candidates',
2542
+ level: 'audit',
2543
+ metadata: this.buildEventMetadata({
1127
2544
  observeType: 'no_candidates',
1128
- sessionId: this.sessionStarted,
1129
- },
2545
+ }),
1130
2546
  });
1131
2547
  (0, metrics_1.timeEnd)('attendant.observe_ms', t0);
1132
2548
  return {
@@ -1134,6 +2550,7 @@ ${detectionWindow}`,
1134
2550
  entitiesDetected: [],
1135
2551
  alreadyPresent: 0,
1136
2552
  totalFound: 0,
2553
+ usageGuidance: buildUsageGuidance('observe'),
1137
2554
  entitiesResolved: [],
1138
2555
  debug: {
1139
2556
  contextLength: currentContext.length,
@@ -1228,37 +2645,51 @@ ${detectionWindow}`,
1228
2645
  const allEntries = await (0, queries_1.findEntriesByEntity)(resolvedInfo.entityType, resolvedInfo.entityId);
1229
2646
  // Priority keys first
1230
2647
  const policyPriorityKeys = policy.observeKeyPriority?.[resolvedInfo.entityType] ?? [];
1231
- const priorityKeys = new Set([...policyPriorityKeys, ...requestedPriorityKeys]);
2648
+ const priorityKeys = new Set(expandContinuityPriorityKeys([...policyPriorityKeys, ...requestedPriorityKeys]));
1232
2649
  const priorityEntries = allEntries.filter((e) => priorityKeys.has(e.key));
1233
2650
  const remainingEntries = allEntries
1234
2651
  .filter((e) => !priorityKeys.has(e.key))
1235
- .sort((a, b) => b.confidence - a.confidence);
2652
+ .sort((a, b) => {
2653
+ const checkpointPenalty = (entryKey) => entryKey.startsWith('checkpoint_') ? 1 : 0;
2654
+ return (checkpointPenalty(a.key) - checkpointPenalty(b.key)
2655
+ || b.confidence - a.confidence
2656
+ || a.key.localeCompare(b.key));
2657
+ });
1236
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
+ }
1237
2665
  for (const entry of selectedEntries) {
1238
2666
  allFacts.push({
1239
- entityKey: `${resolvedInfo.entityType}/${resolvedInfo.entityId}/${entry.key}`,
2667
+ entityKey: `${resolvedInfo.entityType}/${resolvedInfo.entityId}/${normalizeContinuityKey(entry.key)}`,
1240
2668
  summary: entry.valueSummary,
1241
2669
  value: entry.valueRaw,
1242
2670
  confidence: entry.confidence,
1243
2671
  source: entry.source,
2672
+ lastUpdated: entry.updatedAt.toISOString(),
1244
2673
  entryId: entry.id,
1245
2674
  });
1246
2675
  }
1247
2676
  }
1248
- // Step 3 — filter out facts already present in context
2677
+ // Step 3 — filter out facts already present in context (skipped when forceInject)
1249
2678
  const contextLower = currentContext.toLowerCase();
1250
2679
  let alreadyPresent = 0;
1251
2680
  const newFacts = [];
1252
- for (const fact of allFacts) {
1253
- // Check if summary key words appear in context
1254
- const summaryWords = fact.summary.toLowerCase().split(' ').filter((w) => w.length > 4);
1255
- const alreadyInContext = summaryWords.length > 0 &&
1256
- summaryWords.filter((w) => contextLower.includes(w)).length >= Math.ceil(summaryWords.length * 0.6);
1257
- if (alreadyInContext) {
1258
- alreadyPresent++;
1259
- }
1260
- else {
1261
- newFacts.push(fact);
2681
+ if (input.skipContextFilter) {
2682
+ newFacts.push(...allFacts);
2683
+ }
2684
+ else {
2685
+ for (const fact of allFacts) {
2686
+ const alreadyInContext = factAlreadyPresentInContext(contextLower, fact);
2687
+ if (alreadyInContext) {
2688
+ alreadyPresent++;
2689
+ }
2690
+ else {
2691
+ newFacts.push(fact);
2692
+ }
1262
2693
  }
1263
2694
  }
1264
2695
  // Step 4 — return top facts by confidence
@@ -1270,27 +2701,29 @@ ${detectionWindow}`,
1270
2701
  staffComponent: 'Attendant',
1271
2702
  actionType: 'observe_completed',
1272
2703
  agentId: this.agentId,
1273
- source: 'internal',
1274
- reason: null,
1275
- level: 'debug',
1276
- metadata: {
2704
+ source: this.eventSource,
2705
+ reason: topFacts.length > 0 ? 'facts_retrieved' : 'no_new_facts',
2706
+ level: 'audit',
2707
+ metadata: this.buildEventMetadata({
1277
2708
  observeType: 'facts_retrieved',
1278
2709
  factsCount: topFacts.length,
1279
- sessionId: this.sessionStarted,
1280
- },
2710
+ }),
1281
2711
  });
1282
2712
  (0, metrics_1.timeEnd)('attendant.observe_ms', t0);
1283
2713
  return {
1284
- 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,
1285
2716
  entityKey,
1286
2717
  summary,
1287
2718
  value,
1288
2719
  confidence,
1289
2720
  source,
1290
- })),
2721
+ lastUpdated,
2722
+ }))),
1291
2723
  entitiesDetected: Array.from(entitiesDetected),
1292
2724
  alreadyPresent,
1293
2725
  totalFound: allFacts.length,
2726
+ usageGuidance: buildUsageGuidance('observe'),
1294
2727
  entitiesResolved,
1295
2728
  debug: {
1296
2729
  contextLength: currentContext.length,
@@ -1339,6 +2772,10 @@ ${detectionWindow}`,
1339
2772
  explanation: heuristic.explanation,
1340
2773
  };
1341
2774
  }
2775
+ const advisoryDecision = this.buildAdvisoryMemoryDecision(input.latestMessage);
2776
+ if (advisoryDecision) {
2777
+ return advisoryDecision;
2778
+ }
1342
2779
  const contextWindow = input.currentContext.length <= MEMORY_DECISION_CONTEXT_WINDOW_CHARS
1343
2780
  ? input.currentContext
1344
2781
  : input.currentContext.slice(-MEMORY_DECISION_CONTEXT_WINDOW_CHARS);
@@ -1357,8 +2794,9 @@ Return ONLY valid JSON with this exact shape:
1357
2794
  {"needsMemory":true,"confidence":0.81,"reason":"short_reason"}
1358
2795
 
1359
2796
  Rules:
1360
- - needsMemory=true when the answer likely depends on user-specific or session-specific facts.
1361
- - needsMemory=false for generic chit-chat, open-domain facts, or when no memory lookup is needed.
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.
1362
2800
  - confidence is a float from 0 to 1.`,
1363
2801
  },
1364
2802
  ], 128);
@@ -1371,6 +2809,61 @@ Rules:
1371
2809
  explanation: parsed.reason,
1372
2810
  };
1373
2811
  }
2812
+ return this.buildParseFailureFallbackDecision(input);
2813
+ }
2814
+ buildParseFailureFallbackDecision(input) {
2815
+ const normalized = normalizeMessage(input.latestMessage);
2816
+ if (!normalized || MEMORY_NEED_NEGATIVE_PATTERNS.some((pattern) => pattern.test(normalized))) {
2817
+ return {
2818
+ needed: false,
2819
+ confidence: 0.5,
2820
+ method: 'heuristic',
2821
+ explanation: 'classification_parse_failed_default_false',
2822
+ };
2823
+ }
2824
+ // Re-run the heuristic against the strengthened positive patterns. Since the heuristic
2825
+ // was already called in decideMemoryNeed() and returned null (ambiguous), we reach here
2826
+ // only for very short messages (≤20 chars) with no prior pattern matches. The updated
2827
+ // MEMORY_NEED_POSITIVE_PATTERNS now catch most imperative and technical cues, so a second
2828
+ // pass here picks up any stragglers added after the initial call.
2829
+ const heuristicResult = heuristicMemoryNeed(input.latestMessage);
2830
+ if (heuristicResult.needed !== null) {
2831
+ return {
2832
+ needed: heuristicResult.needed,
2833
+ confidence: heuristicResult.confidence,
2834
+ method: 'heuristic',
2835
+ explanation: heuristicResult.needed
2836
+ ? 'classification_parse_failed_heuristic_true'
2837
+ : 'classification_parse_failed_heuristic_false',
2838
+ };
2839
+ }
2840
+ const substantivePrompt = normalized.length >= 12
2841
+ || normalized.split(/\s+/).filter(Boolean).length >= 3
2842
+ || /[?]$/.test(normalized);
2843
+ const hasScopedContext = input.entityHintCount > 0 || input.currentContext.trim().length > 0;
2844
+ const hasProjectStateCue = MEMORY_PARSE_FAILURE_PROJECT_CUE_PATTERNS.some((pattern) => pattern.test(normalized));
2845
+ if (substantivePrompt && (hasScopedContext || hasProjectStateCue)) {
2846
+ return {
2847
+ needed: true,
2848
+ confidence: 0.55,
2849
+ method: 'heuristic',
2850
+ explanation: 'classification_parse_failed_default_true',
2851
+ };
2852
+ }
2853
+ // Final safety net: for any non-trivial message (≥5 chars) where the LLM parse failed,
2854
+ // default to injecting memory rather than silently skipping. A false positive (unnecessary
2855
+ // injection) is far cheaper than a false negative (missing context causes wrong output).
2856
+ // Only true single-word acks/greetings — already caught by MEMORY_NEED_NEGATIVE_PATTERNS
2857
+ // above — should reach this point with a truly empty normalized form; everything else
2858
+ // gets injection.
2859
+ if (normalized.length > 0) {
2860
+ return {
2861
+ needed: true,
2862
+ confidence: 0.5,
2863
+ method: 'heuristic',
2864
+ explanation: 'classification_parse_failed_safe_default_true',
2865
+ };
2866
+ }
1374
2867
  return {
1375
2868
  needed: false,
1376
2869
  confidence: 0.5,
@@ -1378,11 +2871,37 @@ Rules:
1378
2871
  explanation: 'classification_parse_failed_default_false',
1379
2872
  };
1380
2873
  }
2874
+ buildAdvisoryMemoryDecision(latestMessage) {
2875
+ const profile = this.advisoryLearningProfile;
2876
+ const normalized = normalizeMessage(latestMessage);
2877
+ if (!profile?.preferMemoryForAmbiguousTurns || !normalized) {
2878
+ return null;
2879
+ }
2880
+ if (MEMORY_NEED_NEGATIVE_PATTERNS.some((pattern) => pattern.test(normalized))) {
2881
+ return null;
2882
+ }
2883
+ if (normalized.length < 5) {
2884
+ return null;
2885
+ }
2886
+ if (!messageHasAdvisoryCue(normalized, profile.matchedTaskType ?? this.brief?.inferredTaskType ?? null)) {
2887
+ return null;
2888
+ }
2889
+ return {
2890
+ needed: true,
2891
+ confidence: profile.scopesUsed.includes('task') ? 0.78 : 0.7,
2892
+ method: 'advisory',
2893
+ explanation: `advisory_${profile.scopesUsed[0] ?? 'global'}_learning`,
2894
+ };
2895
+ }
1381
2896
  resolveAttendEntityHints(entityHints, latestMessage) {
1382
2897
  const explicit = Array.isArray(entityHints)
1383
2898
  ? entityHints.filter((hint) => typeof hint === 'string' && hint.trim().length > 0)
1384
2899
  : [];
2900
+ const explicitFromMessage = extractExactEntityReferences(latestMessage);
1385
2901
  const scope = (0, autoRemember_1.classifyMemoryScope)(latestMessage);
2902
+ if (explicitFromMessage.length > 0) {
2903
+ return explicitFromMessage;
2904
+ }
1386
2905
  if (explicit.length > 0) {
1387
2906
  return explicit;
1388
2907
  }
@@ -1397,6 +2916,215 @@ Rules:
1397
2916
  }
1398
2917
  return [];
1399
2918
  }
2919
+ resolveObserveEntityHints(entityHints, currentContext) {
2920
+ const explicit = Array.isArray(entityHints)
2921
+ ? entityHints.filter((hint) => typeof hint === 'string' && hint.trim().length > 0)
2922
+ : [];
2923
+ if (explicit.length > 0) {
2924
+ return explicit;
2925
+ }
2926
+ const explicitFromContext = extractExactEntityReferences(currentContext);
2927
+ if (explicitFromContext.length > 0) {
2928
+ return explicitFromContext;
2929
+ }
2930
+ const scope = (0, autoRemember_1.classifyMemoryScope)(currentContext);
2931
+ if (scope && this.brief) {
2932
+ const watched = normalizeWatchedEntities([
2933
+ ...(this.brief?.watchedEntities ?? []),
2934
+ ...(this.sessionCheckpoint?.checkpoint.entityTargets ?? []),
2935
+ ]);
2936
+ if (watched.length > 0) {
2937
+ return watched;
2938
+ }
2939
+ }
2940
+ if (scope === 'personal') {
2941
+ return (0, autoRemember_1.getPersonalRecallEntities)();
2942
+ }
2943
+ if (scope === 'project') {
2944
+ const configured = (0, autoRemember_1.getProjectMemoryEntity)();
2945
+ if (configured) {
2946
+ return [configured];
2947
+ }
2948
+ }
2949
+ return [];
2950
+ }
2951
+ async detectRelevantFreshState(entityHints, latestMessage) {
2952
+ const pending = this.consumePendingFreshState(entityHints, latestMessage);
2953
+ if (pending.hasFreshState) {
2954
+ return pending;
2955
+ }
2956
+ const sinceRaw = this.sharedStateObservedAt?.trim() || this.brief?.briefGeneratedAt?.trim();
2957
+ if (!sinceRaw) {
2958
+ return { hasFreshState: false, priorityKeys: [], entities: [] };
2959
+ }
2960
+ const since = new Date(sinceRaw);
2961
+ if (Number.isNaN(since.getTime())) {
2962
+ return { hasFreshState: false, priorityKeys: [], entities: [] };
2963
+ }
2964
+ const targetHints = entityHints.length > 0
2965
+ ? entityHints
2966
+ : shouldUseWatchedEntitiesForPrompt(latestMessage)
2967
+ ? (this.brief?.watchedEntities ?? [])
2968
+ : [];
2969
+ if (targetHints.length === 0) {
2970
+ return { hasFreshState: false, priorityKeys: [], entities: [] };
2971
+ }
2972
+ const resolvedEntities = await this.expandRelevantFreshTargets(targetHints.slice(0, 5));
2973
+ const entities = [];
2974
+ const priorityKeys = [];
2975
+ const seenKeys = new Set();
2976
+ for (const [canonicalEntity, resolved] of resolvedEntities) {
2977
+ const entries = await (0, queries_1.findEntriesByEntity)(resolved.entityType, resolved.entityId);
2978
+ const freshEntries = entries
2979
+ .filter((entry) => entry.updatedAt > since)
2980
+ .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
2981
+ if (freshEntries.length === 0)
2982
+ continue;
2983
+ entities.push(canonicalEntity);
2984
+ for (const entry of freshEntries) {
2985
+ if (seenKeys.has(entry.key))
2986
+ continue;
2987
+ seenKeys.add(entry.key);
2988
+ priorityKeys.push(entry.key);
2989
+ if (priorityKeys.length >= 8) {
2990
+ break;
2991
+ }
2992
+ }
2993
+ if (priorityKeys.length >= 8) {
2994
+ break;
2995
+ }
2996
+ }
2997
+ return {
2998
+ hasFreshState: entities.length > 0,
2999
+ priorityKeys,
3000
+ entities,
3001
+ };
3002
+ }
3003
+ async expandRelevantFreshTargets(targetHints) {
3004
+ const resolvedEntities = new Map();
3005
+ for (const hint of targetHints) {
3006
+ const resolved = await this.resolveFreshEntityTarget(hint);
3007
+ if (!resolved)
3008
+ continue;
3009
+ resolvedEntities.set(resolved.canonicalEntity, resolved);
3010
+ const related = await (0, relationships_1.getRelated)(resolved.entityType, resolved.entityId);
3011
+ for (const neighbor of related.slice(0, 6)) {
3012
+ const relatedEntity = `${neighbor.entityType}/${neighbor.entityId}`;
3013
+ const relatedResolved = await this.resolveFreshEntityTarget(relatedEntity);
3014
+ if (!relatedResolved)
3015
+ continue;
3016
+ resolvedEntities.set(relatedResolved.canonicalEntity, relatedResolved);
3017
+ if (resolvedEntities.size >= 12) {
3018
+ return resolvedEntities;
3019
+ }
3020
+ }
3021
+ }
3022
+ return resolvedEntities;
3023
+ }
3024
+ async resolveFreshEntityTarget(hint) {
3025
+ try {
3026
+ const parsed = (0, entity_resolution_1.parseEntityString)(hint);
3027
+ try {
3028
+ const resolved = await (0, entity_resolution_1.resolveEntity)({
3029
+ entityType: parsed.entityType,
3030
+ entityId: parsed.entityId,
3031
+ rawName: hint,
3032
+ aliases: [hint, parsed.entityId],
3033
+ source: 'attend_refresh',
3034
+ confidence: 100,
3035
+ createIfMissing: false,
3036
+ });
3037
+ return {
3038
+ canonicalEntity: resolved.canonicalEntity,
3039
+ entityType: resolved.entityType,
3040
+ entityId: resolved.entityId,
3041
+ };
3042
+ }
3043
+ catch {
3044
+ return {
3045
+ canonicalEntity: `${parsed.entityType}/${parsed.entityId}`,
3046
+ entityType: parsed.entityType,
3047
+ entityId: parsed.entityId,
3048
+ };
3049
+ }
3050
+ }
3051
+ catch {
3052
+ return null;
3053
+ }
3054
+ }
3055
+ updateWatchedEntities(candidates) {
3056
+ if (!this.brief)
3057
+ return false;
3058
+ const next = normalizeWatchedEntities([
3059
+ ...(this.brief.watchedEntities ?? []),
3060
+ ...candidates,
3061
+ ]);
3062
+ const changed = JSON.stringify(next) !== JSON.stringify(this.brief.watchedEntities ?? []);
3063
+ this.brief.watchedEntities = next;
3064
+ return changed;
3065
+ }
3066
+ isWatchingEntity(entity) {
3067
+ const normalized = entity.trim();
3068
+ if (!normalized)
3069
+ return false;
3070
+ if ((this.brief?.watchedEntities ?? []).includes(normalized)) {
3071
+ return true;
3072
+ }
3073
+ return (this.sessionCheckpoint?.checkpoint.entityTargets ?? []).includes(normalized);
3074
+ }
3075
+ notifySharedEntityUpdated(entity, key) {
3076
+ const normalizedEntity = entity.trim();
3077
+ const normalizedKey = key.trim();
3078
+ if (!normalizedEntity || !normalizedKey)
3079
+ return;
3080
+ if (!this.isWatchingEntity(normalizedEntity))
3081
+ return;
3082
+ const existing = this.pendingSharedStateInvalidations.get(normalizedEntity) ?? new Set();
3083
+ existing.add(normalizedKey);
3084
+ this.pendingSharedStateInvalidations.set(normalizedEntity, existing);
3085
+ }
3086
+ consumePendingFreshState(entityHints, latestMessage) {
3087
+ const targetHints = entityHints.length > 0
3088
+ ? entityHints
3089
+ : shouldUseWatchedEntitiesForPrompt(latestMessage)
3090
+ ? (this.brief?.watchedEntities ?? [])
3091
+ : [];
3092
+ if (targetHints.length === 0) {
3093
+ return { hasFreshState: false, priorityKeys: [], entities: [] };
3094
+ }
3095
+ const entities = [];
3096
+ const priorityKeys = [];
3097
+ const seenKeys = new Set();
3098
+ for (const entity of targetHints) {
3099
+ const keys = this.pendingSharedStateInvalidations.get(entity);
3100
+ if (!keys || keys.size === 0)
3101
+ continue;
3102
+ entities.push(entity);
3103
+ for (const key of keys) {
3104
+ if (seenKeys.has(key))
3105
+ continue;
3106
+ seenKeys.add(key);
3107
+ priorityKeys.push(key);
3108
+ if (priorityKeys.length >= 8)
3109
+ break;
3110
+ }
3111
+ if (priorityKeys.length >= 8)
3112
+ break;
3113
+ }
3114
+ return {
3115
+ hasFreshState: entities.length > 0,
3116
+ priorityKeys,
3117
+ entities,
3118
+ };
3119
+ }
3120
+ markSharedStateObserved(entities) {
3121
+ if (entities.length === 0)
3122
+ return;
3123
+ this.sharedStateObservedAt = new Date().toISOString();
3124
+ for (const entity of entities) {
3125
+ this.pendingSharedStateInvalidations.delete(entity);
3126
+ }
3127
+ }
1400
3128
  parseMemoryDecision(raw) {
1401
3129
  try {
1402
3130
  const cleaned = raw.replace(/```json|```/g, '').trim();
@@ -1502,6 +3230,11 @@ If nothing is relevant, return: none`,
1502
3230
  async persistState() {
1503
3231
  if (!this.brief)
1504
3232
  return;
3233
+ this.brief = {
3234
+ ...this.brief,
3235
+ compliance: this.buildComplianceState(),
3236
+ pendingMemoryAttributions: this.pendingMemoryAttributions.map((entry) => ({ ...entry })),
3237
+ };
1505
3238
  await (0, client_1.getDb)().knowledgeEntry.upsert({
1506
3239
  where: {
1507
3240
  entityType_entityId_key: {
@@ -1536,6 +3269,16 @@ If nothing is relevant, return: none`,
1536
3269
  this.sessionStarted = state.sessionStarted;
1537
3270
  this.contextCallCount = state.contextCallCount ?? 0;
1538
3271
  this.sessionCheckpoint = state.sessionCheckpoint ?? null;
3272
+ this.advisoryLearningProfile = null;
3273
+ this.sharedStateObservedAt = state.briefGeneratedAt;
3274
+ this.attendsWithoutPersist = state.compliance?.counters.attendsWithoutPersist ?? 0;
3275
+ this.consecutivePreResponseWithoutPost = state.compliance?.counters.consecutivePreResponseWithoutPost ?? 0;
3276
+ this.consecutiveUnusedMemoryInjections = state.compliance?.counters.consecutiveUnusedMemoryInjections ?? 0;
3277
+ this.lastAttendPhase = state.compliance?.counters.lastAttendPhase ?? undefined;
3278
+ this.complianceUpdatedAt = state.compliance?.lastUpdated ?? state.briefGeneratedAt;
3279
+ this.pendingMemoryAttributions = Array.isArray(state.pendingMemoryAttributions)
3280
+ ? state.pendingMemoryAttributions.map((entry) => ({ ...entry }))
3281
+ : [];
1539
3282
  this.brief = state;
1540
3283
  return state;
1541
3284
  }