@memberjunction/ai-agents 5.40.2 → 5.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +53 -0
  2. package/dist/AgentRunner.d.ts +5 -2
  3. package/dist/AgentRunner.d.ts.map +1 -1
  4. package/dist/AgentRunner.js +14 -4
  5. package/dist/AgentRunner.js.map +1 -1
  6. package/dist/MemoryWriteManager.d.ts +188 -0
  7. package/dist/MemoryWriteManager.d.ts.map +1 -0
  8. package/dist/MemoryWriteManager.js +299 -0
  9. package/dist/MemoryWriteManager.js.map +1 -0
  10. package/dist/agent-context-injector.d.ts +29 -0
  11. package/dist/agent-context-injector.d.ts.map +1 -1
  12. package/dist/agent-context-injector.js +90 -32
  13. package/dist/agent-context-injector.js.map +1 -1
  14. package/dist/agent-memory-context-builder.d.ts +100 -0
  15. package/dist/agent-memory-context-builder.d.ts.map +1 -0
  16. package/dist/agent-memory-context-builder.js +172 -0
  17. package/dist/agent-memory-context-builder.js.map +1 -0
  18. package/dist/agent-types/index.d.ts +1 -0
  19. package/dist/agent-types/index.d.ts.map +1 -1
  20. package/dist/agent-types/index.js +1 -0
  21. package/dist/agent-types/index.js.map +1 -1
  22. package/dist/agent-types/loop-agent-response-type.d.ts +12 -1
  23. package/dist/agent-types/loop-agent-response-type.d.ts.map +1 -1
  24. package/dist/agent-types/loop-agent-response-type.js.map +1 -1
  25. package/dist/agent-types/loop-agent-type.d.ts.map +1 -1
  26. package/dist/agent-types/loop-agent-type.js +4 -0
  27. package/dist/agent-types/loop-agent-type.js.map +1 -1
  28. package/dist/agent-types/realtime-agent-type.d.ts +146 -0
  29. package/dist/agent-types/realtime-agent-type.d.ts.map +1 -0
  30. package/dist/agent-types/realtime-agent-type.js +176 -0
  31. package/dist/agent-types/realtime-agent-type.js.map +1 -0
  32. package/dist/base-agent.d.ts +386 -39
  33. package/dist/base-agent.d.ts.map +1 -1
  34. package/dist/base-agent.js +1121 -261
  35. package/dist/base-agent.js.map +1 -1
  36. package/dist/index.d.ts +13 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +17 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/memory-manager-agent.d.ts +99 -4
  41. package/dist/memory-manager-agent.d.ts.map +1 -1
  42. package/dist/memory-manager-agent.js +349 -117
  43. package/dist/memory-manager-agent.js.map +1 -1
  44. package/dist/realtime/bridge-realtime-session-factory.d.ts +111 -0
  45. package/dist/realtime/bridge-realtime-session-factory.d.ts.map +1 -0
  46. package/dist/realtime/bridge-realtime-session-factory.js +163 -0
  47. package/dist/realtime/bridge-realtime-session-factory.js.map +1 -0
  48. package/dist/realtime/bridge-room-transcript-sink.d.ts +58 -0
  49. package/dist/realtime/bridge-room-transcript-sink.d.ts.map +1 -0
  50. package/dist/realtime/bridge-room-transcript-sink.js +127 -0
  51. package/dist/realtime/bridge-room-transcript-sink.js.map +1 -0
  52. package/dist/realtime/meeting-controls-channel-server.d.ts +198 -0
  53. package/dist/realtime/meeting-controls-channel-server.d.ts.map +1 -0
  54. package/dist/realtime/meeting-controls-channel-server.js +319 -0
  55. package/dist/realtime/meeting-controls-channel-server.js.map +1 -0
  56. package/dist/realtime/meeting-controls-state.d.ts +191 -0
  57. package/dist/realtime/meeting-controls-state.d.ts.map +1 -0
  58. package/dist/realtime/meeting-controls-state.js +219 -0
  59. package/dist/realtime/meeting-controls-state.js.map +1 -0
  60. package/dist/realtime/realtime-channel-server-host.d.ts +166 -0
  61. package/dist/realtime/realtime-channel-server-host.d.ts.map +1 -0
  62. package/dist/realtime/realtime-channel-server-host.js +378 -0
  63. package/dist/realtime/realtime-channel-server-host.js.map +1 -0
  64. package/dist/realtime/realtime-client-session-service.d.ts +1026 -0
  65. package/dist/realtime/realtime-client-session-service.d.ts.map +1 -0
  66. package/dist/realtime/realtime-client-session-service.js +1607 -0
  67. package/dist/realtime/realtime-client-session-service.js.map +1 -0
  68. package/dist/realtime/realtime-coagent-config.d.ts +258 -0
  69. package/dist/realtime/realtime-coagent-config.d.ts.map +1 -0
  70. package/dist/realtime/realtime-coagent-config.js +408 -0
  71. package/dist/realtime/realtime-coagent-config.js.map +1 -0
  72. package/dist/realtime/realtime-narration.d.ts +67 -0
  73. package/dist/realtime/realtime-narration.d.ts.map +1 -0
  74. package/dist/realtime/realtime-narration.js +127 -0
  75. package/dist/realtime/realtime-narration.js.map +1 -0
  76. package/dist/realtime/realtime-session-runner.d.ts +383 -0
  77. package/dist/realtime/realtime-session-runner.d.ts.map +1 -0
  78. package/dist/realtime/realtime-session-runner.js +532 -0
  79. package/dist/realtime/realtime-session-runner.js.map +1 -0
  80. package/dist/realtime/realtime-tool-broker.d.ts +294 -0
  81. package/dist/realtime/realtime-tool-broker.d.ts.map +1 -0
  82. package/dist/realtime/realtime-tool-broker.js +206 -0
  83. package/dist/realtime/realtime-tool-broker.js.map +1 -0
  84. package/dist/realtime/whiteboard-channel-server.d.ts +50 -0
  85. package/dist/realtime/whiteboard-channel-server.d.ts.map +1 -0
  86. package/dist/realtime/whiteboard-channel-server.js +85 -0
  87. package/dist/realtime/whiteboard-channel-server.js.map +1 -0
  88. package/package.json +17 -17
@@ -7,7 +7,8 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var MemoryManagerAgent_1;
8
8
  import { BaseAgent } from './base-agent.js';
9
9
  import { RegisterClass, CleanAndParseJSON } from '@memberjunction/global';
10
- import { RunView, RunQuery, LogError, LogStatus } from '@memberjunction/core';
10
+ import { RunView, RunQuery, LogError, LogStatus, LogStatusEx } from '@memberjunction/core';
11
+ import { IsInjectableNoteStatus } from '@memberjunction/core-entities';
11
12
  import { AIPromptRunner } from '@memberjunction/ai-prompts';
12
13
  import { AIPromptParams } from '@memberjunction/ai-core-plus';
13
14
  import { AIEngine } from '@memberjunction/aiengine';
@@ -27,6 +28,10 @@ const EXTRACTION_CONFIG = {
27
28
  * Configuration for note consolidation.
28
29
  * Consolidation finds clusters of similar notes and synthesizes them into single comprehensive notes.
29
30
  */
31
+ const HARDENING_CONFIG = {
32
+ maxNotesPerRun: 200, // Batch cap per MM run — remaining provisional notes picked up next cycle
33
+ dedupeSimilarity: 0.85, // Similarity threshold for the dedupe check against hardened notes
34
+ };
30
35
  const CONSOLIDATION_CONFIG = {
31
36
  /**
32
37
  * How often to run consolidation:
@@ -117,6 +122,8 @@ const DETERMINISTIC_PROMPT_PARAMS = { temperature: 0 };
117
122
  * passes the visited check (e.g., extremely deep legitimate chains from a bug).
118
123
  */
119
124
  const MAX_SOURCE_RESOLUTION_DEPTH = 10;
125
+ /** Fields fetched for a SourceRunScope projection. */
126
+ const SOURCE_RUN_SCOPE_FIELDS = ['AgentID', 'CompanyID', 'UserID', 'PrimaryScopeEntityID', 'PrimaryScopeRecordID', 'SecondaryScopes'];
120
127
  /**
121
128
  * Memory Manager Agent - automatically extracts notes and examples from conversations.
122
129
  * This agent runs on a schedule (every 15 minutes) and analyzes high-quality conversations
@@ -209,13 +216,19 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
209
216
  * For first run, returns null to process all history (limited by MaxRows).
210
217
  */
211
218
  async GetLastRunTime(agentId, contextUser) {
219
+ // Read-only: we only need StartedAt, so use a 'simple' projection with a narrow Fields
220
+ // list. Loading this as 'entity_object' would build a full MJAIAgentRunEntityExtended,
221
+ // whose InnerLoad fires a secondary "MJ: AI Agent Run Steps" RunView (LoadRelatedData) —
222
+ // a round-trip the redundancy telemetry flagged as a duplicate. A simple projection
223
+ // skips that sub-query entirely.
212
224
  const rv = new RunView();
213
225
  const result = await rv.RunView({
214
226
  EntityName: 'MJ: AI Agent Runs',
215
227
  ExtraFilter: `AgentID='${agentId}' AND Status='Completed'`,
216
228
  OrderBy: 'StartedAt DESC',
217
229
  MaxRows: 1,
218
- ResultType: 'entity_object'
230
+ Fields: ['StartedAt'],
231
+ ResultType: 'simple'
219
232
  }, contextUser);
220
233
  if (result.Success && result.Results && result.Results.length > 0) {
221
234
  // Ensure we return a proper Date object
@@ -225,6 +238,25 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
225
238
  // First run - return null to process all history (with MaxRows limit)
226
239
  return null;
227
240
  }
241
+ /**
242
+ * Load a source agent run's scope fields (read-only) for scope inheritance, memoized in
243
+ * the supplied per-cycle cache. Uses a 'simple' projection so it does not trigger the
244
+ * agent-run entity's LoadRelatedData steps sub-query.
245
+ */
246
+ async loadSourceRunScope(sourceAgentRunId, runCache, contextUser) {
247
+ if (!runCache.has(sourceAgentRunId)) {
248
+ const rv = new RunView();
249
+ const runResult = await rv.RunView({
250
+ EntityName: 'MJ: AI Agent Runs',
251
+ ExtraFilter: `ID='${sourceAgentRunId}'`,
252
+ MaxRows: 1,
253
+ Fields: [...SOURCE_RUN_SCOPE_FIELDS],
254
+ ResultType: 'simple'
255
+ }, contextUser);
256
+ runCache.set(sourceAgentRunId, runResult.Success && runResult.Results?.length > 0 ? runResult.Results[0] : null);
257
+ }
258
+ return runCache.get(sourceAgentRunId) || null;
259
+ }
228
260
  /**
229
261
  * Load agents that have note or example injection enabled.
230
262
  * Only extract notes/examples for agents that actually use these features.
@@ -275,7 +307,9 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
275
307
  MaxRows: 1000 // Limit to 1000 conversations per run
276
308
  }, contextUser);
277
309
  if (!result.Success || !result.Results || result.Results.length === 0) {
278
- LogStatus('Memory Manager: No conversations with new activity found');
310
+ // Verbose-only: the common no-op outcome. The engine's "✅ Completed (Nms)" line already
311
+ // signals the run finished with no work; surfacing "why" is a verbose diagnostic detail.
312
+ LogStatusEx({ message: 'Memory Manager: No conversations with new activity found', verboseOnly: true });
279
313
  return [];
280
314
  }
281
315
  // Parse the query results into ConversationWithRatings objects
@@ -319,6 +353,12 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
319
353
  /**
320
354
  * Load agent runs with high-usage artifacts since last run.
321
355
  * Links through: ArtifactUse -> ArtifactVersion -> ConversationDetailArtifact -> ConversationDetail -> Conversation -> AIAgentRun
356
+ *
357
+ * The caller only consumes the row count (`.length`) of the returned set, so this reads a
358
+ * 'simple' ID-only projection rather than full entity objects. Loading these as
359
+ * 'entity_object' would build up to 50 MJAIAgentRunEntityExtended instances, each firing a
360
+ * secondary "MJ: AI Agent Run Steps" RunView via InnerLoad/LoadRelatedData — exactly the
361
+ * redundant round-trips the telemetry flagged. The simple projection skips all of them.
322
362
  */
323
363
  async LoadHighValueAgentRuns(since, contextUser) {
324
364
  const rv = new RunView();
@@ -350,7 +390,8 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
350
390
  ExtraFilter: filter,
351
391
  OrderBy: '__mj_CreatedAt DESC',
352
392
  MaxRows: 50, // Limit to most recent 50 high-value runs
353
- ResultType: 'entity_object'
393
+ Fields: ['ID'],
394
+ ResultType: 'simple'
354
395
  }, contextUser);
355
396
  return result.Success ? (result.Results || []) : [];
356
397
  }
@@ -848,8 +889,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
848
889
  let merged = 0;
849
890
  let failed = 0;
850
891
  const md = this.ProviderToUse;
851
- const rv = new RunView();
852
- // Cache source agent runs to avoid repeated lookups
892
+ // Cache source agent runs (scope fields only) to avoid repeated lookups
853
893
  const runCache = new Map();
854
894
  // Get the "AI" note type ID for AI-generated notes
855
895
  const aiNoteTypeId = AIEngine.Instance.AgenteNoteTypeIDByName('AI');
@@ -867,16 +907,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
867
907
  // Load source agent run for scope inheritance (if available)
868
908
  let sourceRun = null;
869
909
  if (extracted.sourceAgentRunId) {
870
- if (!runCache.has(extracted.sourceAgentRunId)) {
871
- const runResult = await rv.RunView({
872
- EntityName: 'MJ: AI Agent Runs',
873
- ExtraFilter: `ID='${extracted.sourceAgentRunId}'`,
874
- MaxRows: 1,
875
- ResultType: 'entity_object'
876
- }, contextUser);
877
- runCache.set(extracted.sourceAgentRunId, runResult.Success && runResult.Results?.length > 0 ? runResult.Results[0] : null);
878
- }
879
- sourceRun = runCache.get(extracted.sourceAgentRunId) || null;
910
+ sourceRun = await this.loadSourceRunScope(extracted.sourceAgentRunId, runCache, contextUser);
880
911
  }
881
912
  // Check if we should merge with existing (revoke all targets, create one replacement)
882
913
  let createNewNote = !extracted.mergeWithExistingIds?.length;
@@ -1202,7 +1233,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
1202
1233
  * Returns both a boolean and the reason it fired so the observability layer can
1203
1234
  * record the trigger type on the parent consolidation run step.
1204
1235
  */
1205
- async shouldRunConsolidation(agentId, contextUser, forceMaintenance) {
1236
+ async shouldRunConsolidation(agentId, contextUser, forceMaintenance, lastRun) {
1206
1237
  if (forceMaintenance)
1207
1238
  return { shouldRun: true, triggerType: 'forced' };
1208
1239
  const freq = CONSOLIDATION_CONFIG.frequency;
@@ -1211,7 +1242,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
1211
1242
  if (freq === 'every-run')
1212
1243
  return { shouldRun: true, triggerType: 'every-run' };
1213
1244
  if (freq === 'hourly' || freq === 'daily') {
1214
- return this.shouldRunConsolidationByTimeWindow(agentId, contextUser, freq === 'hourly' ? 1 : 24);
1245
+ return this.shouldRunConsolidationByTimeWindow(contextUser, freq === 'hourly' ? 1 : 24, lastRun);
1215
1246
  }
1216
1247
  if (typeof freq === 'number') {
1217
1248
  return this.shouldRunConsolidationByRunCount(agentId, contextUser, freq);
@@ -1224,8 +1255,11 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
1224
1255
  * when the event-driven new-note threshold has been crossed. Falls back to
1225
1256
  * triggerType='not-triggered' when neither condition fires.
1226
1257
  */
1227
- async shouldRunConsolidationByTimeWindow(agentId, contextUser, thresholdHours) {
1228
- const lastRun = await this.GetLastRunTime(agentId, contextUser);
1258
+ async shouldRunConsolidationByTimeWindow(contextUser, thresholdHours, lastRun) {
1259
+ // `lastRun` is the same value computed once at the top of the maintenance cycle
1260
+ // (executeAgentInternal). Threading it through here removes a second identical
1261
+ // `MJ: AI Agent Runs` RunView (AgentID + Status='Completed', OrderBy StartedAt DESC)
1262
+ // that the redundancy telemetry flagged as a duplicate round-trip.
1229
1263
  if (!lastRun)
1230
1264
  return { shouldRun: true, triggerType: 'time' };
1231
1265
  const hoursSinceLast = (Date.now() - lastRun.getTime()) / (1000 * 60 * 60);
@@ -1234,20 +1268,26 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
1234
1268
  // Event-driven trigger: consolidate when at least `noteCountTrigger` new notes
1235
1269
  // have been added system-wide since the last Memory Manager run.
1236
1270
  //
1237
- // NOTE: this query intentionally does NOT filter by AgentID. The `agentId` parameter
1271
+ // NOTE: this count intentionally does NOT filter by AgentID. The `agentId` parameter
1238
1272
  // here is the Memory Manager's own ID (passed from `params.agent.ID` at the call
1239
1273
  // site), not the agent that owns the notes. Filtering the count by MM's own agent
1240
1274
  // ID would never match anything because MM doesn't own notes — it processes notes
1241
1275
  // belonging to other (memory-enabled) agents.
1242
- const rv = new RunView();
1243
- const newNoteResult = await rv.RunView({
1244
- EntityName: 'MJ: AI Agent Notes',
1245
- ExtraFilter: `IsAutoGenerated=1 AND Status='Active' AND __mj_CreatedAt > '${lastRun.toISOString()}'`,
1246
- ResultType: 'count_only'
1247
- }, contextUser);
1248
- if (newNoteResult.Success && (newNoteResult.TotalRowCount || 0) >= CONSOLIDATION_CONFIG.noteCountTrigger) {
1276
+ //
1277
+ // AIEngineBase caches `MJ: AI Agent Notes` unfiltered as full entity objects
1278
+ // (`AIEngine.Instance.AgentNotes`) and keeps that cache current via BaseEntity
1279
+ // save/delete events, so we count in-memory instead of a `count_only` RunView. This
1280
+ // avoids the "Entity Already in Engine" redundancy telemetry warning and a DB
1281
+ // round-trip on every maintenance cycle. Mirrors the cached-note filtering used
1282
+ // elsewhere in this class (e.g. buildMaintenanceSummary).
1283
+ const lastRunMs = lastRun.getTime();
1284
+ const newNoteCount = AIEngine.Instance.AgentNotes.filter(n => n.IsAutoGenerated &&
1285
+ n.Status === 'Active' &&
1286
+ n.__mj_CreatedAt != null &&
1287
+ new Date(n.__mj_CreatedAt).getTime() > lastRunMs).length;
1288
+ if (newNoteCount >= CONSOLIDATION_CONFIG.noteCountTrigger) {
1249
1289
  if (this._verbose) {
1250
- LogStatus(`Memory Manager: Event-driven consolidation trigger fired (${newNoteResult.TotalRowCount} new notes >= ${CONSOLIDATION_CONFIG.noteCountTrigger} threshold since last MM run)`);
1290
+ LogStatus(`Memory Manager: Event-driven consolidation trigger fired (${newNoteCount} new notes >= ${CONSOLIDATION_CONFIG.noteCountTrigger} threshold since last MM run)`);
1251
1291
  }
1252
1292
  return { shouldRun: true, triggerType: 'event' };
1253
1293
  }
@@ -1420,8 +1460,57 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
1420
1460
  }
1421
1461
  return originals;
1422
1462
  }
1423
- /** Load a batch of notes by ID with the fields needed for source resolution. */
1463
+ /**
1464
+ * Load a batch of notes by ID with the fields needed for source resolution.
1465
+ *
1466
+ * AIEngineBase caches `MJ: AI Agent Notes` unfiltered as full entity objects
1467
+ * (`AIEngine.Instance.AgentNotes`), so source-chain ancestors — which are pre-existing
1468
+ * notes, never ones created earlier in this same cycle — are already in memory. We serve
1469
+ * those from the cache and only issue a RunView for IDs the cache doesn't have, instead of
1470
+ * a DB round-trip per BFS depth level. This resolves the redundancy telemetry's
1471
+ * "Entity Already in Engine" warning for AIEngineBase while preserving exact behavior
1472
+ * (the projection shape and field set are unchanged).
1473
+ */
1424
1474
  async loadResolvedNotes(noteIds, contextUser) {
1475
+ const cachedById = new Map();
1476
+ for (const note of AIEngine.Instance.AgentNotes) {
1477
+ if (note.ID)
1478
+ cachedById.set(note.ID.toLowerCase(), note);
1479
+ }
1480
+ const resolved = [];
1481
+ const missingIds = [];
1482
+ for (const id of noteIds) {
1483
+ const cached = cachedById.get(id.toLowerCase());
1484
+ if (cached) {
1485
+ resolved.push(this.projectResolvedNote(cached));
1486
+ }
1487
+ else {
1488
+ missingIds.push(id);
1489
+ }
1490
+ }
1491
+ if (missingIds.length > 0) {
1492
+ resolved.push(...await this.loadResolvedNotesFromDatabase(missingIds, contextUser));
1493
+ }
1494
+ return resolved;
1495
+ }
1496
+ /** Project a cached note entity into the read-only shape used by source resolution. */
1497
+ projectResolvedNote(note) {
1498
+ return {
1499
+ ID: note.ID,
1500
+ ConsolidationCount: note.ConsolidationCount,
1501
+ DerivedFromNoteIDs: note.DerivedFromNoteIDs,
1502
+ Note: note.Note,
1503
+ Type: note.Type,
1504
+ AccessCount: note.AccessCount,
1505
+ ImportanceScore: note.ImportanceScore,
1506
+ AgentID: note.AgentID,
1507
+ UserID: note.UserID,
1508
+ CompanyID: note.CompanyID,
1509
+ __mj_CreatedAt: note.__mj_CreatedAt
1510
+ };
1511
+ }
1512
+ /** RunView fallback for note IDs not present in the AIEngine cache. */
1513
+ async loadResolvedNotesFromDatabase(noteIds, contextUser) {
1425
1514
  const resolvedFields = [
1426
1515
  'ID', 'ConsolidationCount', 'DerivedFromNoteIDs', 'Note', 'Type',
1427
1516
  'AccessCount', 'ImportanceScore', 'AgentID', 'UserID', 'CompanyID', '__mj_CreatedAt'
@@ -1907,7 +1996,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
1907
1996
  async pruneStaleReferences(contextUser) {
1908
1997
  const maxPerCycle = STALE_PRUNING_CONFIG.maxNotesPerRun;
1909
1998
  const result = { notesArchived: 0, orphanedAgents: 0, orphanedUsers: 0, orphanedCompanies: 0, orphanedConversations: 0 };
1910
- const notes = await this.loadPruneCandidateNotes(contextUser, maxPerCycle * 2);
1999
+ const notes = this.loadPruneCandidateNotes(maxPerCycle * 2);
1911
2000
  if (notes.length === 0) {
1912
2001
  return result;
1913
2002
  }
@@ -1924,22 +2013,33 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
1924
2013
  return result;
1925
2014
  }
1926
2015
  /**
1927
- * Load auto-generated, non-immutable Active notes that are candidates for orphan pruning.
2016
+ * Collect auto-generated, non-immutable `Active` notes that are candidates for orphan
2017
+ * pruning, oldest first.
2018
+ *
2019
+ * Served from the `AIEngine` cache rather than a `RunView`: `AIEngine.Instance.AgentNotes`
2020
+ * holds the full note pool, loaded unfiltered as entity objects and kept current via
2021
+ * `BaseEntity` save/delete events, so an equivalent in-memory filter/sort/cap avoids both a
2022
+ * DB round-trip and the "Entity Already in Engine" redundancy-telemetry warning each cycle.
2023
+ * Note `.filter()` returns a fresh array, so the subsequent `.sort()` never reorders the
2024
+ * engine's cached array. The result is projected to plain {@link PruneCandidateNote} rows —
2025
+ * we never hand a cached `BaseEntity` instance to a mutating caller; `pruneStaleReferences`
2026
+ * re-`Load()`s a fresh, owned entity before archiving.
2027
+ *
2028
+ * @param maxRows Hard cap on candidates returned this cycle (oldest-`__mj_CreatedAt` first).
2029
+ * @returns Lightweight projection rows for orphan-reference evaluation.
1928
2030
  */
1929
- async loadPruneCandidateNotes(contextUser, maxRows) {
1930
- const rv = new RunView();
1931
- const notesResult = await rv.RunView({
1932
- EntityName: 'MJ: AI Agent Notes',
1933
- ExtraFilter: `IsAutoGenerated = 1 AND Status = 'Active' AND ProtectionTier != 'Immutable'`,
1934
- Fields: ['ID', 'AgentID', 'UserID', 'CompanyID', 'SourceConversationID'],
1935
- OrderBy: '__mj_CreatedAt ASC',
1936
- MaxRows: maxRows,
1937
- ResultType: 'simple'
1938
- }, contextUser);
1939
- if (!notesResult.Success || !notesResult.Results?.length) {
1940
- return [];
1941
- }
1942
- return notesResult.Results;
2031
+ loadPruneCandidateNotes(maxRows) {
2032
+ return AIEngine.Instance.AgentNotes
2033
+ .filter(n => n.IsAutoGenerated && n.Status === 'Active' && (n.ProtectionTier || 'Standard') !== 'Immutable')
2034
+ .sort((a, b) => new Date(a.__mj_CreatedAt).getTime() - new Date(b.__mj_CreatedAt).getTime())
2035
+ .slice(0, maxRows)
2036
+ .map(n => ({
2037
+ ID: n.ID,
2038
+ AgentID: n.AgentID,
2039
+ UserID: n.UserID,
2040
+ CompanyID: n.CompanyID,
2041
+ SourceConversationID: n.SourceConversationID
2042
+ }));
1943
2043
  }
1944
2044
  /**
1945
2045
  * Build in-memory lookup caches of which referenced users/companies/conversations still
@@ -2118,7 +2218,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2118
2218
  const result = { notesExpired: 0, examplesExpired: 0 };
2119
2219
  const md = this.ProviderToUse;
2120
2220
  const nowISO = new Date().toISOString();
2121
- const [expiredNotes, expiredExamples] = await this.loadExpiredItems(contextUser, nowISO);
2221
+ const [expiredNotes, expiredExamples] = this.loadExpiredItems(nowISO);
2122
2222
  for (const note of expiredNotes) {
2123
2223
  if ((note.ProtectionTier || 'Standard') === 'Immutable')
2124
2224
  continue;
@@ -2131,26 +2231,36 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2131
2231
  }
2132
2232
  return result;
2133
2233
  }
2134
- async loadExpiredItems(contextUser, nowISO) {
2135
- const rv = new RunView();
2136
- const [notesResult, examplesResult] = await Promise.all([
2137
- rv.RunView({
2138
- EntityName: 'MJ: AI Agent Notes',
2139
- ExtraFilter: `Status = 'Active' AND ExpiresAt IS NOT NULL AND ExpiresAt < '${nowISO}'`,
2140
- Fields: ['ID', 'ExpiresAt', 'ProtectionTier'],
2141
- MaxRows: 200,
2142
- }, contextUser),
2143
- rv.RunView({
2144
- EntityName: 'MJ: AI Agent Examples',
2145
- ExtraFilter: `Status = 'Active' AND ExpiresAt IS NOT NULL AND ExpiresAt < '${nowISO}'`,
2146
- Fields: ['ID', 'ExpiresAt'],
2147
- MaxRows: 200,
2148
- }, contextUser)
2149
- ]);
2150
- return [
2151
- notesResult.Success && notesResult.Results ? notesResult.Results : [],
2152
- examplesResult.Success && examplesResult.Results ? examplesResult.Results : []
2153
- ];
2234
+ /**
2235
+ * Find notes and examples whose explicit `ExpiresAt` timestamp has passed, so the
2236
+ * archival pass can expire them regardless of decay score.
2237
+ *
2238
+ * Served from the `AIEngine` caches (`AgentNotes` / `AgentExamples`) instead of a batched
2239
+ * `RunViews`: both pools are loaded unfiltered as entity objects and kept current via
2240
+ * `BaseEntity` events, so an in-memory filter/cap reproduces the former queries while
2241
+ * removing two DB round-trips and the pair of "Entity Already in Engine" redundancy
2242
+ * warnings they emitted. Rows are projected to plain shapes; `archiveExpiredNote` /
2243
+ * `archiveExpiredExample` re-`Load()` a fresh, owned entity before mutating.
2244
+ *
2245
+ * Notes include `Provisional` (via {@link IsInjectableNoteStatus} — the same Active+Provisional
2246
+ * predicate used by the read-path injection queries) so the TTL safety net on in-flight
2247
+ * agent writes is enforced: a provisional note the hardening pass never reaches must still
2248
+ * expire.
2249
+ *
2250
+ * @param nowISO Current time as an ISO string; items with `ExpiresAt` strictly before this are expired.
2251
+ * @returns A `[expiredNotes, expiredExamples]` tuple of projection rows (each capped at 200).
2252
+ */
2253
+ loadExpiredItems(nowISO) {
2254
+ const nowMs = new Date(nowISO).getTime();
2255
+ const expiredNotes = AIEngine.Instance.AgentNotes
2256
+ .filter(n => IsInjectableNoteStatus(n.Status) && n.ExpiresAt != null && new Date(n.ExpiresAt).getTime() < nowMs)
2257
+ .slice(0, 200)
2258
+ .map(n => ({ ID: n.ID, ExpiresAt: n.ExpiresAt, ProtectionTier: n.ProtectionTier || 'Standard' }));
2259
+ const expiredExamples = AIEngine.Instance.AgentExamples
2260
+ .filter(e => e.Status === 'Active' && e.ExpiresAt != null && new Date(e.ExpiresAt).getTime() < nowMs)
2261
+ .slice(0, 200)
2262
+ .map(e => ({ ID: e.ID, ExpiresAt: e.ExpiresAt }));
2263
+ return [expiredNotes, expiredExamples];
2154
2264
  }
2155
2265
  async archiveExpiredNote(note, md, contextUser) {
2156
2266
  const noteToArchive = await md.GetEntityObject('MJ: AI Agent Notes', contextUser);
@@ -2181,7 +2291,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2181
2291
  const result = { notesDecayed: 0, notesArchived: 0, examplesDecayed: 0, examplesArchived: 0 };
2182
2292
  const stats = { decayFactors: [], protectedPreserved: 0, ephemeralAccelerated: 0 };
2183
2293
  const md = this.ProviderToUse;
2184
- const [activeNotes, activeExamples] = await this.loadDecayCandidates(contextUser);
2294
+ const [activeNotes, activeExamples] = this.loadDecayCandidates();
2185
2295
  for (const note of activeNotes) {
2186
2296
  await this.decayOneNote(note, archiveEnabledAgentIds, md, contextUser, result, stats);
2187
2297
  }
@@ -2191,28 +2301,43 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2191
2301
  return { ...result, stats };
2192
2302
  }
2193
2303
  /**
2194
- * Fetch notes and examples in parallel — they're independent.
2304
+ * Fetch decay-candidate notes and examples from the AIEngine caches.
2305
+ *
2306
+ * Served from `AIEngine.Instance.AgentNotes` / `AgentExamples` (loaded unfiltered as
2307
+ * entity objects and kept current via BaseEntity events), filtered/capped in-memory to
2308
+ * mirror the former RunViews — avoids two "Entity Already in Engine" redundancy warnings
2309
+ * and a DB round-trip per maintenance cycle. We project to plain candidate rows; the
2310
+ * actual decay/archival re-Loads fresh entities.
2311
+ *
2312
+ * @returns A `[activeNotes, activeExamples]` tuple of decay-candidate projection rows
2313
+ * (auto-generated `Active` notes capped at 1000; auto-generated `Active` examples
2314
+ * scoring below 50, capped at 500).
2195
2315
  */
2196
- async loadDecayCandidates(contextUser) {
2197
- const rv = new RunView();
2198
- const [activeNotesResult, activeExamplesResult] = await Promise.all([
2199
- rv.RunView({
2200
- EntityName: 'MJ: AI Agent Notes',
2201
- ExtraFilter: `IsAutoGenerated = 1 AND Status = 'Active'`,
2202
- Fields: ['ID', 'ImportanceScore', 'ProtectionTier', 'LastAccessedAt', '__mj_CreatedAt', 'AccessCount', 'AgentID'],
2203
- MaxRows: 1000,
2204
- }, contextUser),
2205
- rv.RunView({
2206
- EntityName: 'MJ: AI Agent Examples',
2207
- ExtraFilter: `IsAutoGenerated = 1 AND Status = 'Active' AND SuccessScore < 50`,
2208
- Fields: ['ID', 'SuccessScore', 'LastAccessedAt', '__mj_CreatedAt', 'AccessCount', 'AgentID'],
2209
- MaxRows: 500,
2210
- }, contextUser)
2211
- ]);
2212
- return [
2213
- activeNotesResult.Success && activeNotesResult.Results ? activeNotesResult.Results : [],
2214
- activeExamplesResult.Success && activeExamplesResult.Results ? activeExamplesResult.Results : []
2215
- ];
2316
+ loadDecayCandidates() {
2317
+ const activeNotes = AIEngine.Instance.AgentNotes
2318
+ .filter(n => n.IsAutoGenerated && n.Status === 'Active')
2319
+ .slice(0, 1000)
2320
+ .map(n => ({
2321
+ ID: n.ID,
2322
+ ImportanceScore: n.ImportanceScore,
2323
+ ProtectionTier: n.ProtectionTier || 'Standard',
2324
+ LastAccessedAt: n.LastAccessedAt,
2325
+ __mj_CreatedAt: n.__mj_CreatedAt,
2326
+ AccessCount: n.AccessCount,
2327
+ AgentID: n.AgentID
2328
+ }));
2329
+ const activeExamples = AIEngine.Instance.AgentExamples
2330
+ .filter(e => e.IsAutoGenerated && e.Status === 'Active' && (e.SuccessScore ?? 0) < 50)
2331
+ .slice(0, 500)
2332
+ .map(e => ({
2333
+ ID: e.ID,
2334
+ SuccessScore: e.SuccessScore,
2335
+ LastAccessedAt: e.LastAccessedAt,
2336
+ __mj_CreatedAt: e.__mj_CreatedAt,
2337
+ AccessCount: e.AccessCount,
2338
+ AgentID: e.AgentID
2339
+ }));
2340
+ return [activeNotes, activeExamples];
2216
2341
  }
2217
2342
  /**
2218
2343
  * Apply proportional decay to a single note. Archives if the decayed score drops below
@@ -2282,6 +2407,110 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2282
2407
  if (await exampleToArchive.Save())
2283
2408
  result.examplesArchived++;
2284
2409
  }
2410
+ /**
2411
+ * Hardening pass over in-flight agent-written notes (Status='Provisional',
2412
+ * AuthorType='Agent'). Runs UNCONDITIONALLY at the start of every MM cycle —
2413
+ * before the consolidation-gated maintenance phases — so that:
2414
+ * 1. provisional notes never languish waiting for a consolidation trigger, and
2415
+ * 2. notes hardened here participate in importance scoring, consolidation,
2416
+ * contradiction detection, and decay in the SAME cycle via the existing
2417
+ * machinery (no duplicated reconciliation logic in this pass).
2418
+ *
2419
+ * Per note: dedupe against hardened (Active) notes using the same LLM dedupe
2420
+ * prompt as extraction — duplicates are Archived with ConsolidatedIntoNoteID
2421
+ * lineage; survivors are hardened (Status='Active', ExpiresAt=NULL so the
2422
+ * standard decay machinery owns their lifetime).
2423
+ */
2424
+ async runHardeningPhase(contextUser) {
2425
+ const counters = { hardened: 0, deduped: 0, failed: 0 };
2426
+ const step = await this.CreateRunStep('Decision', 'Harden Provisional Notes', {
2427
+ maxNotesPerRun: HARDENING_CONFIG.maxNotesPerRun
2428
+ });
2429
+ try {
2430
+ // Served from AIEngine.Instance.AgentNotes (loaded unfiltered as entity
2431
+ // objects and kept current via BaseEntity + remote-invalidate events),
2432
+ // filtered/ordered/capped in-memory to mirror the former RunView. This
2433
+ // avoids the "Entity Already in Engine" redundancy warning and a DB
2434
+ // round-trip every MM cycle, matching loadDecayCandidates/loadExpiredItems.
2435
+ // Provisional agent notes are in-flight writes already reflected in the
2436
+ // cache, and hardenSingleNote mutates+saves these entities directly —
2437
+ // consistent with the dedupe path, which already saves cached note
2438
+ // instances returned by FindSimilarAgentNotes.
2439
+ const provisionalNotes = AIEngine.Instance.AgentNotes
2440
+ .filter(n => n.Status === 'Provisional' && n.AuthorType === 'Agent')
2441
+ .sort((a, b) => new Date(a.__mj_CreatedAt).getTime() - new Date(b.__mj_CreatedAt).getTime())
2442
+ .slice(0, HARDENING_CONFIG.maxNotesPerRun);
2443
+ if (provisionalNotes.length === 0) {
2444
+ await this.FinalizeRunStep(step, true, { ...counters, provisionalCount: 0 });
2445
+ return counters;
2446
+ }
2447
+ const runner = new AIPromptRunner();
2448
+ const dedupePrompt = AIEngine.Instance.Prompts.find(p => p.Name === 'Memory Manager - Deduplicate Note' && p.Category === 'MJ: System');
2449
+ for (const note of provisionalNotes) {
2450
+ const outcome = await this.hardenSingleNote(note, dedupePrompt, runner, contextUser);
2451
+ counters[outcome]++;
2452
+ }
2453
+ if (this._verbose) {
2454
+ LogStatus(`Memory Manager: Hardening pass — ${counters.hardened} hardened, ${counters.deduped} deduped, ${counters.failed} failed of ${provisionalNotes.length} provisional notes`);
2455
+ }
2456
+ await this.FinalizeRunStep(step, counters.failed === 0, { ...counters, provisionalCount: provisionalNotes.length });
2457
+ }
2458
+ catch (error) {
2459
+ LogError('Memory Manager: Hardening pass failed, continuing with run:', error);
2460
+ await this.FinalizeRunStep(step, false, counters, undefined, error instanceof Error ? error.message : String(error));
2461
+ }
2462
+ return counters;
2463
+ }
2464
+ /**
2465
+ * Harden one provisional note: archive it as a duplicate when the LLM dedupe
2466
+ * decision says an equivalent hardened note exists, otherwise promote it to
2467
+ * Active with decay-managed lifetime.
2468
+ */
2469
+ async hardenSingleNote(note, dedupePrompt, runner, contextUser) {
2470
+ try {
2471
+ // Compare against HARDENED notes only — other provisional notes are themselves
2472
+ // unvetted, and same-run duplicates were already handled at write time.
2473
+ const similar = (await AIEngine.Instance.FindSimilarAgentNotes(note.Note || '', note.AgentID || undefined, note.UserID || undefined, note.CompanyID || undefined, 10, HARDENING_CONFIG.dedupeSimilarity)).filter(m => m.note.ID !== note.ID && m.note.Status === 'Active');
2474
+ if (similar.length > 0 && dedupePrompt) {
2475
+ const candidate = {
2476
+ type: note.Type,
2477
+ content: note.Note || '',
2478
+ confidence: 100,
2479
+ agentId: note.AgentID || undefined,
2480
+ userId: note.UserID || undefined,
2481
+ companyId: note.CompanyID || undefined,
2482
+ };
2483
+ const decision = await this.runDedupePromptForCandidate(runner, dedupePrompt, candidate, similar, contextUser);
2484
+ if (!decision.shouldAdd) {
2485
+ const target = similar[0].note;
2486
+ note.Status = 'Archived';
2487
+ note.ConsolidatedIntoNoteID = target.ID;
2488
+ note.Comments = `${note.Comments || ''} [Hardening: duplicate of ${target.ID} — ${decision.reason}]`.trim();
2489
+ if (!await note.Save()) {
2490
+ LogError(`Memory Manager: Failed to archive duplicate provisional note ${note.ID}: ${note.LatestResult?.CompleteMessage || 'unknown error'}`);
2491
+ return 'failed';
2492
+ }
2493
+ target.AccessCount = (target.AccessCount || 0) + 1;
2494
+ target.LastAccessedAt = new Date();
2495
+ if (!await target.Save()) {
2496
+ LogError(`Memory Manager: Failed to bump dedupe target ${target.ID}: ${target.LatestResult?.CompleteMessage || 'unknown error'}`);
2497
+ }
2498
+ return 'deduped';
2499
+ }
2500
+ }
2501
+ note.Status = 'Active';
2502
+ note.ExpiresAt = null;
2503
+ if (!await note.Save()) {
2504
+ LogError(`Memory Manager: Failed to harden provisional note ${note.ID}: ${note.LatestResult?.CompleteMessage || 'unknown error'}`);
2505
+ return 'failed';
2506
+ }
2507
+ return 'hardened';
2508
+ }
2509
+ catch (error) {
2510
+ LogError(`Memory Manager: Exception hardening note ${note.ID}:`, error);
2511
+ return 'failed';
2512
+ }
2513
+ }
2285
2514
  /**
2286
2515
  * Run all 5 maintenance phases in order: importance scoring, consolidation,
2287
2516
  * contradiction detection, stale-reference pruning, and decay-based archival.
@@ -2483,24 +2712,14 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2483
2712
  let skipped = 0;
2484
2713
  let failed = 0;
2485
2714
  const md = this.ProviderToUse;
2486
- const rv = new RunView();
2487
- // Cache source agent runs to avoid repeated lookups
2715
+ // Cache source agent runs (scope fields only) to avoid repeated lookups
2488
2716
  const runCache = new Map();
2489
2717
  for (const extracted of extractedExamples) {
2490
2718
  try {
2491
2719
  // Load source agent run for scope inheritance (if available)
2492
2720
  let sourceRun = null;
2493
2721
  if (extracted.sourceAgentRunId) {
2494
- if (!runCache.has(extracted.sourceAgentRunId)) {
2495
- const runResult = await rv.RunView({
2496
- EntityName: 'MJ: AI Agent Runs',
2497
- ExtraFilter: `ID='${extracted.sourceAgentRunId}'`,
2498
- MaxRows: 1,
2499
- ResultType: 'entity_object'
2500
- }, contextUser);
2501
- runCache.set(extracted.sourceAgentRunId, runResult.Success && runResult.Results?.length > 0 ? runResult.Results[0] : null);
2502
- }
2503
- sourceRun = runCache.get(extracted.sourceAgentRunId) || null;
2722
+ sourceRun = await this.loadSourceRunScope(extracted.sourceAgentRunId, runCache, contextUser);
2504
2723
  }
2505
2724
  const example = await md.GetEntityObject('MJ: AI Agent Examples', contextUser);
2506
2725
  // AgentID must come from source run - LLM doesn't know real agent IDs
@@ -2578,10 +2797,15 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2578
2797
  this._agentRunID = this.AgentRun?.ID || null;
2579
2798
  this._stepCounter = 0;
2580
2799
  this._contextUser = params.contextUser || null;
2581
- LogStatus('Memory Manager: Starting analysis cycle');
2582
- const lastRunTime = await this.GetLastRunTime(params.agent.ID, params.contextUser);
2583
- // Load agents that have memory injection enabled
2584
- const agentsUsingMemory = await this.LoadAgentsUsingMemory(params.contextUser);
2800
+ // Verbose-only: the scheduling engine's always-on "▶️ Starting / ✅ Completed" lines already
2801
+ // mark that this run began/ended. On routine no-op runs this internal trace is just noise.
2802
+ LogStatusEx({ message: 'Memory Manager: Starting analysis cycle', verboseOnly: true });
2803
+ // Phase 1: the last-run-time and the memory-enabled agent set are independent — load in
2804
+ // parallel (avoids two back-to-back sequential RunViews flagged by the parallelization telemetry).
2805
+ const [lastRunTime, agentsUsingMemory] = await Promise.all([
2806
+ this.GetLastRunTime(params.agent.ID, params.contextUser),
2807
+ this.LoadAgentsUsingMemory(params.contextUser)
2808
+ ]);
2585
2809
  if (this._verbose) {
2586
2810
  const sinceMessage = lastRunTime ? `since ${lastRunTime.toISOString()}` : 'all history';
2587
2811
  LogStatus(`Memory Manager: Processing ${sinceMessage}`);
@@ -2595,13 +2819,21 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2595
2819
  };
2596
2820
  return { finalStep, stepCount: 1 };
2597
2821
  }
2598
- // Step 1: Load conversations with new activity (includes rating data)
2822
+ // Phase 2: conversations-with-new-activity and high-value agent runs both depend only on
2823
+ // (lastRunTime, agentsUsingMemory) — never on each other — so load them in parallel, each
2824
+ // recorded in its own run step.
2599
2825
  const step1 = await this.CreateRunStep('Decision', 'Load Conversations With New Activity', {
2600
2826
  since: lastRunTime?.toISOString() || null,
2601
2827
  agentCount: agentsUsingMemory.length,
2602
2828
  agentIds: agentsUsingMemory.map(a => a.ID)
2603
2829
  });
2604
- const conversations = await this.LoadConversationsWithNewActivity(lastRunTime, agentsUsingMemory, params.contextUser);
2830
+ const step2 = await this.CreateRunStep('Decision', 'Load High-Value Agent Runs', {
2831
+ since: lastRunTime?.toISOString() || null
2832
+ });
2833
+ const [conversations, agentRuns] = await Promise.all([
2834
+ this.LoadConversationsWithNewActivity(lastRunTime, agentsUsingMemory, params.contextUser),
2835
+ this.LoadHighValueAgentRuns(lastRunTime, params.contextUser)
2836
+ ]);
2605
2837
  const totalMessages = conversations.reduce((sum, c) => sum + c.messages.length, 0);
2606
2838
  await this.FinalizeRunStep(step1, true, {
2607
2839
  conversationCount: conversations.length,
@@ -2610,18 +2842,11 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2610
2842
  negativeCount: conversations.filter(c => c.hasNegativeRating).length,
2611
2843
  unratedCount: conversations.filter(c => c.isUnrated).length
2612
2844
  });
2613
- if (this._verbose) {
2614
- LogStatus(`Memory Manager: Found ${conversations.length} conversations with new activity`);
2615
- }
2616
- // Step 2: Load high-value agent runs
2617
- const step2 = await this.CreateRunStep('Decision', 'Load High-Value Agent Runs', {
2618
- since: lastRunTime?.toISOString() || null
2619
- });
2620
- const agentRuns = await this.LoadHighValueAgentRuns(lastRunTime, params.contextUser);
2621
2845
  await this.FinalizeRunStep(step2, true, {
2622
2846
  runCount: agentRuns.length
2623
2847
  });
2624
2848
  if (this._verbose) {
2849
+ LogStatus(`Memory Manager: Found ${conversations.length} conversations with new activity`);
2625
2850
  LogStatus(`Memory Manager: Found ${agentRuns.length} high-value agent runs`);
2626
2851
  }
2627
2852
  const hasNewData = conversations.length > 0 || agentRuns.length > 0;
@@ -2691,8 +2916,15 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2691
2916
  if (this._verbose)
2692
2917
  LogStatus(`Memory Manager: Created ${notesCreated} notes and ${examplesCreated} examples`);
2693
2918
  }
2919
+ // Harden agent-written provisional notes FIRST and unconditionally — survivors
2920
+ // become Active and participate in this same cycle's maintenance phases
2921
+ // (importance, consolidation, contradiction, decay) when those fire.
2922
+ const hardening = await this.runHardeningPhase(params.contextUser);
2923
+ const hardeningSummary = hardening.hardened + hardening.deduped + hardening.failed > 0
2924
+ ? ` Hardened ${hardening.hardened} provisional notes (${hardening.deduped} deduped, ${hardening.failed} failed).`
2925
+ : '';
2694
2926
  const forceMaintenance = params.data?.forceMaintenance === true;
2695
- const triggerDecision = await this.shouldRunConsolidation(params.agent.ID, params.contextUser, forceMaintenance);
2927
+ const triggerDecision = await this.shouldRunConsolidation(params.agent.ID, params.contextUser, forceMaintenance, lastRunTime);
2696
2928
  const maintenance = triggerDecision.shouldRun
2697
2929
  ? await this.runMaintenancePhases(params.contextUser, forceMaintenance, triggerDecision.triggerType)
2698
2930
  : { consolidatedCount: 0, consolidationArchived: 0, contradictionsFound: 0, contradictionsResolved: 0, contradictionsFlagged: 0, staleNotesArchived: 0, decayNotesArchived: 0, decayExamplesArchived: 0, importanceScored: 0, tierPromotions: 0 };
@@ -2700,7 +2932,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2700
2932
  const finalStep = {
2701
2933
  terminate: true,
2702
2934
  step: 'Success',
2703
- message: `Processed ${conversations.length} conversations (${totalMessages} messages) and ${agentRuns.length} agent runs. Created ${notesCreated} notes and ${examplesCreated} examples.${maintenanceSummary}`,
2935
+ message: `Processed ${conversations.length} conversations (${totalMessages} messages) and ${agentRuns.length} agent runs. Created ${notesCreated} notes and ${examplesCreated} examples.${hardeningSummary}${maintenanceSummary}`,
2704
2936
  newPayload: {
2705
2937
  notesCreated,
2706
2938
  examplesCreated,