@memberjunction/ai-agents 5.40.2 → 5.41.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 (80) hide show
  1. package/README.md +45 -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 +365 -24
  33. package/dist/base-agent.d.ts.map +1 -1
  34. package/dist/base-agent.js +995 -175
  35. package/dist/base-agent.js.map +1 -1
  36. package/dist/index.d.ts +11 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +15 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/memory-manager-agent.d.ts +55 -2
  41. package/dist/memory-manager-agent.d.ts.map +1 -1
  42. package/dist/memory-manager-agent.js +261 -62
  43. package/dist/memory-manager-agent.js.map +1 -1
  44. package/dist/realtime/meeting-controls-channel-server.d.ts +198 -0
  45. package/dist/realtime/meeting-controls-channel-server.d.ts.map +1 -0
  46. package/dist/realtime/meeting-controls-channel-server.js +319 -0
  47. package/dist/realtime/meeting-controls-channel-server.js.map +1 -0
  48. package/dist/realtime/meeting-controls-state.d.ts +191 -0
  49. package/dist/realtime/meeting-controls-state.d.ts.map +1 -0
  50. package/dist/realtime/meeting-controls-state.js +219 -0
  51. package/dist/realtime/meeting-controls-state.js.map +1 -0
  52. package/dist/realtime/realtime-channel-server-host.d.ts +166 -0
  53. package/dist/realtime/realtime-channel-server-host.d.ts.map +1 -0
  54. package/dist/realtime/realtime-channel-server-host.js +378 -0
  55. package/dist/realtime/realtime-channel-server-host.js.map +1 -0
  56. package/dist/realtime/realtime-client-session-service.d.ts +884 -0
  57. package/dist/realtime/realtime-client-session-service.d.ts.map +1 -0
  58. package/dist/realtime/realtime-client-session-service.js +1401 -0
  59. package/dist/realtime/realtime-client-session-service.js.map +1 -0
  60. package/dist/realtime/realtime-coagent-config.d.ts +202 -0
  61. package/dist/realtime/realtime-coagent-config.d.ts.map +1 -0
  62. package/dist/realtime/realtime-coagent-config.js +334 -0
  63. package/dist/realtime/realtime-coagent-config.js.map +1 -0
  64. package/dist/realtime/realtime-narration.d.ts +67 -0
  65. package/dist/realtime/realtime-narration.d.ts.map +1 -0
  66. package/dist/realtime/realtime-narration.js +127 -0
  67. package/dist/realtime/realtime-narration.js.map +1 -0
  68. package/dist/realtime/realtime-session-runner.d.ts +383 -0
  69. package/dist/realtime/realtime-session-runner.d.ts.map +1 -0
  70. package/dist/realtime/realtime-session-runner.js +532 -0
  71. package/dist/realtime/realtime-session-runner.js.map +1 -0
  72. package/dist/realtime/realtime-tool-broker.d.ts +279 -0
  73. package/dist/realtime/realtime-tool-broker.d.ts.map +1 -0
  74. package/dist/realtime/realtime-tool-broker.js +184 -0
  75. package/dist/realtime/realtime-tool-broker.js.map +1 -0
  76. package/dist/realtime/whiteboard-channel-server.d.ts +50 -0
  77. package/dist/realtime/whiteboard-channel-server.d.ts.map +1 -0
  78. package/dist/realtime/whiteboard-channel-server.js +85 -0
  79. package/dist/realtime/whiteboard-channel-server.js.map +1 -0
  80. package/package.json +17 -17
@@ -8,6 +8,7 @@ var MemoryManagerAgent_1;
8
8
  import { BaseAgent } from './base-agent.js';
9
9
  import { RegisterClass, CleanAndParseJSON } from '@memberjunction/global';
10
10
  import { RunView, RunQuery, LogError, LogStatus } from '@memberjunction/core';
11
+ import { InjectableNoteStatusSQLList } 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.
@@ -319,6 +351,12 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
319
351
  /**
320
352
  * Load agent runs with high-usage artifacts since last run.
321
353
  * Links through: ArtifactUse -> ArtifactVersion -> ConversationDetailArtifact -> ConversationDetail -> Conversation -> AIAgentRun
354
+ *
355
+ * The caller only consumes the row count (`.length`) of the returned set, so this reads a
356
+ * 'simple' ID-only projection rather than full entity objects. Loading these as
357
+ * 'entity_object' would build up to 50 MJAIAgentRunEntityExtended instances, each firing a
358
+ * secondary "MJ: AI Agent Run Steps" RunView via InnerLoad/LoadRelatedData — exactly the
359
+ * redundant round-trips the telemetry flagged. The simple projection skips all of them.
322
360
  */
323
361
  async LoadHighValueAgentRuns(since, contextUser) {
324
362
  const rv = new RunView();
@@ -350,7 +388,8 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
350
388
  ExtraFilter: filter,
351
389
  OrderBy: '__mj_CreatedAt DESC',
352
390
  MaxRows: 50, // Limit to most recent 50 high-value runs
353
- ResultType: 'entity_object'
391
+ Fields: ['ID'],
392
+ ResultType: 'simple'
354
393
  }, contextUser);
355
394
  return result.Success ? (result.Results || []) : [];
356
395
  }
@@ -848,8 +887,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
848
887
  let merged = 0;
849
888
  let failed = 0;
850
889
  const md = this.ProviderToUse;
851
- const rv = new RunView();
852
- // Cache source agent runs to avoid repeated lookups
890
+ // Cache source agent runs (scope fields only) to avoid repeated lookups
853
891
  const runCache = new Map();
854
892
  // Get the "AI" note type ID for AI-generated notes
855
893
  const aiNoteTypeId = AIEngine.Instance.AgenteNoteTypeIDByName('AI');
@@ -867,16 +905,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
867
905
  // Load source agent run for scope inheritance (if available)
868
906
  let sourceRun = null;
869
907
  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;
908
+ sourceRun = await this.loadSourceRunScope(extracted.sourceAgentRunId, runCache, contextUser);
880
909
  }
881
910
  // Check if we should merge with existing (revoke all targets, create one replacement)
882
911
  let createNewNote = !extracted.mergeWithExistingIds?.length;
@@ -1202,7 +1231,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
1202
1231
  * Returns both a boolean and the reason it fired so the observability layer can
1203
1232
  * record the trigger type on the parent consolidation run step.
1204
1233
  */
1205
- async shouldRunConsolidation(agentId, contextUser, forceMaintenance) {
1234
+ async shouldRunConsolidation(agentId, contextUser, forceMaintenance, lastRun) {
1206
1235
  if (forceMaintenance)
1207
1236
  return { shouldRun: true, triggerType: 'forced' };
1208
1237
  const freq = CONSOLIDATION_CONFIG.frequency;
@@ -1211,7 +1240,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
1211
1240
  if (freq === 'every-run')
1212
1241
  return { shouldRun: true, triggerType: 'every-run' };
1213
1242
  if (freq === 'hourly' || freq === 'daily') {
1214
- return this.shouldRunConsolidationByTimeWindow(agentId, contextUser, freq === 'hourly' ? 1 : 24);
1243
+ return this.shouldRunConsolidationByTimeWindow(contextUser, freq === 'hourly' ? 1 : 24, lastRun);
1215
1244
  }
1216
1245
  if (typeof freq === 'number') {
1217
1246
  return this.shouldRunConsolidationByRunCount(agentId, contextUser, freq);
@@ -1224,8 +1253,11 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
1224
1253
  * when the event-driven new-note threshold has been crossed. Falls back to
1225
1254
  * triggerType='not-triggered' when neither condition fires.
1226
1255
  */
1227
- async shouldRunConsolidationByTimeWindow(agentId, contextUser, thresholdHours) {
1228
- const lastRun = await this.GetLastRunTime(agentId, contextUser);
1256
+ async shouldRunConsolidationByTimeWindow(contextUser, thresholdHours, lastRun) {
1257
+ // `lastRun` is the same value computed once at the top of the maintenance cycle
1258
+ // (executeAgentInternal). Threading it through here removes a second identical
1259
+ // `MJ: AI Agent Runs` RunView (AgentID + Status='Completed', OrderBy StartedAt DESC)
1260
+ // that the redundancy telemetry flagged as a duplicate round-trip.
1229
1261
  if (!lastRun)
1230
1262
  return { shouldRun: true, triggerType: 'time' };
1231
1263
  const hoursSinceLast = (Date.now() - lastRun.getTime()) / (1000 * 60 * 60);
@@ -1420,8 +1452,57 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
1420
1452
  }
1421
1453
  return originals;
1422
1454
  }
1423
- /** Load a batch of notes by ID with the fields needed for source resolution. */
1455
+ /**
1456
+ * Load a batch of notes by ID with the fields needed for source resolution.
1457
+ *
1458
+ * AIEngineBase caches `MJ: AI Agent Notes` unfiltered as full entity objects
1459
+ * (`AIEngine.Instance.AgentNotes`), so source-chain ancestors — which are pre-existing
1460
+ * notes, never ones created earlier in this same cycle — are already in memory. We serve
1461
+ * those from the cache and only issue a RunView for IDs the cache doesn't have, instead of
1462
+ * a DB round-trip per BFS depth level. This resolves the redundancy telemetry's
1463
+ * "Entity Already in Engine" warning for AIEngineBase while preserving exact behavior
1464
+ * (the projection shape and field set are unchanged).
1465
+ */
1424
1466
  async loadResolvedNotes(noteIds, contextUser) {
1467
+ const cachedById = new Map();
1468
+ for (const note of AIEngine.Instance.AgentNotes) {
1469
+ if (note.ID)
1470
+ cachedById.set(note.ID.toLowerCase(), note);
1471
+ }
1472
+ const resolved = [];
1473
+ const missingIds = [];
1474
+ for (const id of noteIds) {
1475
+ const cached = cachedById.get(id.toLowerCase());
1476
+ if (cached) {
1477
+ resolved.push(this.projectResolvedNote(cached));
1478
+ }
1479
+ else {
1480
+ missingIds.push(id);
1481
+ }
1482
+ }
1483
+ if (missingIds.length > 0) {
1484
+ resolved.push(...await this.loadResolvedNotesFromDatabase(missingIds, contextUser));
1485
+ }
1486
+ return resolved;
1487
+ }
1488
+ /** Project a cached note entity into the read-only shape used by source resolution. */
1489
+ projectResolvedNote(note) {
1490
+ return {
1491
+ ID: note.ID,
1492
+ ConsolidationCount: note.ConsolidationCount,
1493
+ DerivedFromNoteIDs: note.DerivedFromNoteIDs,
1494
+ Note: note.Note,
1495
+ Type: note.Type,
1496
+ AccessCount: note.AccessCount,
1497
+ ImportanceScore: note.ImportanceScore,
1498
+ AgentID: note.AgentID,
1499
+ UserID: note.UserID,
1500
+ CompanyID: note.CompanyID,
1501
+ __mj_CreatedAt: note.__mj_CreatedAt
1502
+ };
1503
+ }
1504
+ /** RunView fallback for note IDs not present in the AIEngine cache. */
1505
+ async loadResolvedNotesFromDatabase(noteIds, contextUser) {
1425
1506
  const resolvedFields = [
1426
1507
  'ID', 'ConsolidationCount', 'DerivedFromNoteIDs', 'Note', 'Type',
1427
1508
  'AccessCount', 'ImportanceScore', 'AgentID', 'UserID', 'CompanyID', '__mj_CreatedAt'
@@ -2133,22 +2214,27 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2133
2214
  }
2134
2215
  async loadExpiredItems(contextUser, nowISO) {
2135
2216
  const rv = new RunView();
2136
- const [notesResult, examplesResult] = await Promise.all([
2137
- rv.RunView({
2217
+ const [notesResult, examplesResult] = await rv.RunViews([
2218
+ {
2138
2219
  EntityName: 'MJ: AI Agent Notes',
2139
- ExtraFilter: `Status = 'Active' AND ExpiresAt IS NOT NULL AND ExpiresAt < '${nowISO}'`,
2220
+ // Includes Provisional so the TTL safety net on in-flight agent writes is
2221
+ // actually enforced — a provisional note the hardening pass never reaches
2222
+ // must still expire.
2223
+ ExtraFilter: `Status IN (${InjectableNoteStatusSQLList()}) AND ExpiresAt IS NOT NULL AND ExpiresAt < '${nowISO}'`,
2140
2224
  Fields: ['ID', 'ExpiresAt', 'ProtectionTier'],
2141
2225
  MaxRows: 200,
2142
- }, contextUser),
2143
- rv.RunView({
2226
+ },
2227
+ {
2144
2228
  EntityName: 'MJ: AI Agent Examples',
2145
2229
  ExtraFilter: `Status = 'Active' AND ExpiresAt IS NOT NULL AND ExpiresAt < '${nowISO}'`,
2146
2230
  Fields: ['ID', 'ExpiresAt'],
2147
2231
  MaxRows: 200,
2148
- }, contextUser)
2149
- ]);
2232
+ }
2233
+ ], contextUser);
2150
2234
  return [
2151
- notesResult.Success && notesResult.Results ? notesResult.Results : [],
2235
+ notesResult.Success && notesResult.Results
2236
+ ? notesResult.Results.filter((r) => 'ProtectionTier' in r)
2237
+ : [],
2152
2238
  examplesResult.Success && examplesResult.Results ? examplesResult.Results : []
2153
2239
  ];
2154
2240
  }
@@ -2194,24 +2280,32 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2194
2280
  * Fetch notes and examples in parallel — they're independent.
2195
2281
  */
2196
2282
  async loadDecayCandidates(contextUser) {
2283
+ // Batch into one RunViews round-trip — see loadExpiredItems for the rationale on why
2284
+ // two parallel RunView calls are flagged sequential by the redundancy telemetry. The
2285
+ // two projections differ, so the batch is typed with their union and narrowed by
2286
+ // position (notes carry ProtectionTier, examples carry SuccessScore).
2197
2287
  const rv = new RunView();
2198
- const [activeNotesResult, activeExamplesResult] = await Promise.all([
2199
- rv.RunView({
2288
+ const [activeNotesResult, activeExamplesResult] = await rv.RunViews([
2289
+ {
2200
2290
  EntityName: 'MJ: AI Agent Notes',
2201
2291
  ExtraFilter: `IsAutoGenerated = 1 AND Status = 'Active'`,
2202
2292
  Fields: ['ID', 'ImportanceScore', 'ProtectionTier', 'LastAccessedAt', '__mj_CreatedAt', 'AccessCount', 'AgentID'],
2203
2293
  MaxRows: 1000,
2204
- }, contextUser),
2205
- rv.RunView({
2294
+ },
2295
+ {
2206
2296
  EntityName: 'MJ: AI Agent Examples',
2207
2297
  ExtraFilter: `IsAutoGenerated = 1 AND Status = 'Active' AND SuccessScore < 50`,
2208
2298
  Fields: ['ID', 'SuccessScore', 'LastAccessedAt', '__mj_CreatedAt', 'AccessCount', 'AgentID'],
2209
2299
  MaxRows: 500,
2210
- }, contextUser)
2211
- ]);
2300
+ }
2301
+ ], contextUser);
2212
2302
  return [
2213
- activeNotesResult.Success && activeNotesResult.Results ? activeNotesResult.Results : [],
2214
- activeExamplesResult.Success && activeExamplesResult.Results ? activeExamplesResult.Results : []
2303
+ activeNotesResult.Success && activeNotesResult.Results
2304
+ ? activeNotesResult.Results.filter((r) => 'ProtectionTier' in r)
2305
+ : [],
2306
+ activeExamplesResult.Success && activeExamplesResult.Results
2307
+ ? activeExamplesResult.Results.filter((r) => 'SuccessScore' in r)
2308
+ : []
2215
2309
  ];
2216
2310
  }
2217
2311
  /**
@@ -2282,6 +2376,110 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2282
2376
  if (await exampleToArchive.Save())
2283
2377
  result.examplesArchived++;
2284
2378
  }
2379
+ /**
2380
+ * Hardening pass over in-flight agent-written notes (Status='Provisional',
2381
+ * AuthorType='Agent'). Runs UNCONDITIONALLY at the start of every MM cycle —
2382
+ * before the consolidation-gated maintenance phases — so that:
2383
+ * 1. provisional notes never languish waiting for a consolidation trigger, and
2384
+ * 2. notes hardened here participate in importance scoring, consolidation,
2385
+ * contradiction detection, and decay in the SAME cycle via the existing
2386
+ * machinery (no duplicated reconciliation logic in this pass).
2387
+ *
2388
+ * Per note: dedupe against hardened (Active) notes using the same LLM dedupe
2389
+ * prompt as extraction — duplicates are Archived with ConsolidatedIntoNoteID
2390
+ * lineage; survivors are hardened (Status='Active', ExpiresAt=NULL so the
2391
+ * standard decay machinery owns their lifetime).
2392
+ */
2393
+ async runHardeningPhase(contextUser) {
2394
+ const counters = { hardened: 0, deduped: 0, failed: 0 };
2395
+ const step = await this.CreateRunStep('Decision', 'Harden Provisional Notes', {
2396
+ maxNotesPerRun: HARDENING_CONFIG.maxNotesPerRun
2397
+ });
2398
+ try {
2399
+ const rv = new RunView();
2400
+ const result = await rv.RunView({
2401
+ EntityName: 'MJ: AI Agent Notes',
2402
+ ExtraFilter: `Status = 'Provisional' AND AuthorType = 'Agent'`,
2403
+ OrderBy: '__mj_CreatedAt ASC',
2404
+ MaxRows: HARDENING_CONFIG.maxNotesPerRun,
2405
+ ResultType: 'entity_object'
2406
+ }, contextUser);
2407
+ if (!result.Success) {
2408
+ await this.FinalizeRunStep(step, false, counters, undefined, `Failed to load provisional notes: ${result.ErrorMessage}`);
2409
+ return counters;
2410
+ }
2411
+ const provisionalNotes = result.Results || [];
2412
+ if (provisionalNotes.length === 0) {
2413
+ await this.FinalizeRunStep(step, true, { ...counters, provisionalCount: 0 });
2414
+ return counters;
2415
+ }
2416
+ const runner = new AIPromptRunner();
2417
+ const dedupePrompt = AIEngine.Instance.Prompts.find(p => p.Name === 'Memory Manager - Deduplicate Note' && p.Category === 'MJ: System');
2418
+ for (const note of provisionalNotes) {
2419
+ const outcome = await this.hardenSingleNote(note, dedupePrompt, runner, contextUser);
2420
+ counters[outcome]++;
2421
+ }
2422
+ if (this._verbose) {
2423
+ LogStatus(`Memory Manager: Hardening pass — ${counters.hardened} hardened, ${counters.deduped} deduped, ${counters.failed} failed of ${provisionalNotes.length} provisional notes`);
2424
+ }
2425
+ await this.FinalizeRunStep(step, counters.failed === 0, { ...counters, provisionalCount: provisionalNotes.length });
2426
+ }
2427
+ catch (error) {
2428
+ LogError('Memory Manager: Hardening pass failed, continuing with run:', error);
2429
+ await this.FinalizeRunStep(step, false, counters, undefined, error instanceof Error ? error.message : String(error));
2430
+ }
2431
+ return counters;
2432
+ }
2433
+ /**
2434
+ * Harden one provisional note: archive it as a duplicate when the LLM dedupe
2435
+ * decision says an equivalent hardened note exists, otherwise promote it to
2436
+ * Active with decay-managed lifetime.
2437
+ */
2438
+ async hardenSingleNote(note, dedupePrompt, runner, contextUser) {
2439
+ try {
2440
+ // Compare against HARDENED notes only — other provisional notes are themselves
2441
+ // unvetted, and same-run duplicates were already handled at write time.
2442
+ 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');
2443
+ if (similar.length > 0 && dedupePrompt) {
2444
+ const candidate = {
2445
+ type: note.Type,
2446
+ content: note.Note || '',
2447
+ confidence: 100,
2448
+ agentId: note.AgentID || undefined,
2449
+ userId: note.UserID || undefined,
2450
+ companyId: note.CompanyID || undefined,
2451
+ };
2452
+ const decision = await this.runDedupePromptForCandidate(runner, dedupePrompt, candidate, similar, contextUser);
2453
+ if (!decision.shouldAdd) {
2454
+ const target = similar[0].note;
2455
+ note.Status = 'Archived';
2456
+ note.ConsolidatedIntoNoteID = target.ID;
2457
+ note.Comments = `${note.Comments || ''} [Hardening: duplicate of ${target.ID} — ${decision.reason}]`.trim();
2458
+ if (!await note.Save()) {
2459
+ LogError(`Memory Manager: Failed to archive duplicate provisional note ${note.ID}: ${note.LatestResult?.CompleteMessage || 'unknown error'}`);
2460
+ return 'failed';
2461
+ }
2462
+ target.AccessCount = (target.AccessCount || 0) + 1;
2463
+ target.LastAccessedAt = new Date();
2464
+ if (!await target.Save()) {
2465
+ LogError(`Memory Manager: Failed to bump dedupe target ${target.ID}: ${target.LatestResult?.CompleteMessage || 'unknown error'}`);
2466
+ }
2467
+ return 'deduped';
2468
+ }
2469
+ }
2470
+ note.Status = 'Active';
2471
+ note.ExpiresAt = null;
2472
+ if (!await note.Save()) {
2473
+ LogError(`Memory Manager: Failed to harden provisional note ${note.ID}: ${note.LatestResult?.CompleteMessage || 'unknown error'}`);
2474
+ return 'failed';
2475
+ }
2476
+ return 'hardened';
2477
+ }
2478
+ catch (error) {
2479
+ LogError(`Memory Manager: Exception hardening note ${note.ID}:`, error);
2480
+ return 'failed';
2481
+ }
2482
+ }
2285
2483
  /**
2286
2484
  * Run all 5 maintenance phases in order: importance scoring, consolidation,
2287
2485
  * contradiction detection, stale-reference pruning, and decay-based archival.
@@ -2483,24 +2681,14 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2483
2681
  let skipped = 0;
2484
2682
  let failed = 0;
2485
2683
  const md = this.ProviderToUse;
2486
- const rv = new RunView();
2487
- // Cache source agent runs to avoid repeated lookups
2684
+ // Cache source agent runs (scope fields only) to avoid repeated lookups
2488
2685
  const runCache = new Map();
2489
2686
  for (const extracted of extractedExamples) {
2490
2687
  try {
2491
2688
  // Load source agent run for scope inheritance (if available)
2492
2689
  let sourceRun = null;
2493
2690
  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;
2691
+ sourceRun = await this.loadSourceRunScope(extracted.sourceAgentRunId, runCache, contextUser);
2504
2692
  }
2505
2693
  const example = await md.GetEntityObject('MJ: AI Agent Examples', contextUser);
2506
2694
  // AgentID must come from source run - LLM doesn't know real agent IDs
@@ -2579,9 +2767,12 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2579
2767
  this._stepCounter = 0;
2580
2768
  this._contextUser = params.contextUser || null;
2581
2769
  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);
2770
+ // Phase 1: the last-run-time and the memory-enabled agent set are independent — load in
2771
+ // parallel (avoids two back-to-back sequential RunViews flagged by the parallelization telemetry).
2772
+ const [lastRunTime, agentsUsingMemory] = await Promise.all([
2773
+ this.GetLastRunTime(params.agent.ID, params.contextUser),
2774
+ this.LoadAgentsUsingMemory(params.contextUser)
2775
+ ]);
2585
2776
  if (this._verbose) {
2586
2777
  const sinceMessage = lastRunTime ? `since ${lastRunTime.toISOString()}` : 'all history';
2587
2778
  LogStatus(`Memory Manager: Processing ${sinceMessage}`);
@@ -2595,13 +2786,21 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2595
2786
  };
2596
2787
  return { finalStep, stepCount: 1 };
2597
2788
  }
2598
- // Step 1: Load conversations with new activity (includes rating data)
2789
+ // Phase 2: conversations-with-new-activity and high-value agent runs both depend only on
2790
+ // (lastRunTime, agentsUsingMemory) — never on each other — so load them in parallel, each
2791
+ // recorded in its own run step.
2599
2792
  const step1 = await this.CreateRunStep('Decision', 'Load Conversations With New Activity', {
2600
2793
  since: lastRunTime?.toISOString() || null,
2601
2794
  agentCount: agentsUsingMemory.length,
2602
2795
  agentIds: agentsUsingMemory.map(a => a.ID)
2603
2796
  });
2604
- const conversations = await this.LoadConversationsWithNewActivity(lastRunTime, agentsUsingMemory, params.contextUser);
2797
+ const step2 = await this.CreateRunStep('Decision', 'Load High-Value Agent Runs', {
2798
+ since: lastRunTime?.toISOString() || null
2799
+ });
2800
+ const [conversations, agentRuns] = await Promise.all([
2801
+ this.LoadConversationsWithNewActivity(lastRunTime, agentsUsingMemory, params.contextUser),
2802
+ this.LoadHighValueAgentRuns(lastRunTime, params.contextUser)
2803
+ ]);
2605
2804
  const totalMessages = conversations.reduce((sum, c) => sum + c.messages.length, 0);
2606
2805
  await this.FinalizeRunStep(step1, true, {
2607
2806
  conversationCount: conversations.length,
@@ -2610,18 +2809,11 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2610
2809
  negativeCount: conversations.filter(c => c.hasNegativeRating).length,
2611
2810
  unratedCount: conversations.filter(c => c.isUnrated).length
2612
2811
  });
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
2812
  await this.FinalizeRunStep(step2, true, {
2622
2813
  runCount: agentRuns.length
2623
2814
  });
2624
2815
  if (this._verbose) {
2816
+ LogStatus(`Memory Manager: Found ${conversations.length} conversations with new activity`);
2625
2817
  LogStatus(`Memory Manager: Found ${agentRuns.length} high-value agent runs`);
2626
2818
  }
2627
2819
  const hasNewData = conversations.length > 0 || agentRuns.length > 0;
@@ -2691,8 +2883,15 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2691
2883
  if (this._verbose)
2692
2884
  LogStatus(`Memory Manager: Created ${notesCreated} notes and ${examplesCreated} examples`);
2693
2885
  }
2886
+ // Harden agent-written provisional notes FIRST and unconditionally — survivors
2887
+ // become Active and participate in this same cycle's maintenance phases
2888
+ // (importance, consolidation, contradiction, decay) when those fire.
2889
+ const hardening = await this.runHardeningPhase(params.contextUser);
2890
+ const hardeningSummary = hardening.hardened + hardening.deduped + hardening.failed > 0
2891
+ ? ` Hardened ${hardening.hardened} provisional notes (${hardening.deduped} deduped, ${hardening.failed} failed).`
2892
+ : '';
2694
2893
  const forceMaintenance = params.data?.forceMaintenance === true;
2695
- const triggerDecision = await this.shouldRunConsolidation(params.agent.ID, params.contextUser, forceMaintenance);
2894
+ const triggerDecision = await this.shouldRunConsolidation(params.agent.ID, params.contextUser, forceMaintenance, lastRunTime);
2696
2895
  const maintenance = triggerDecision.shouldRun
2697
2896
  ? await this.runMaintenancePhases(params.contextUser, forceMaintenance, triggerDecision.triggerType)
2698
2897
  : { consolidatedCount: 0, consolidationArchived: 0, contradictionsFound: 0, contradictionsResolved: 0, contradictionsFlagged: 0, staleNotesArchived: 0, decayNotesArchived: 0, decayExamplesArchived: 0, importanceScored: 0, tierPromotions: 0 };
@@ -2700,7 +2899,7 @@ let MemoryManagerAgent = MemoryManagerAgent_1 = class MemoryManagerAgent extends
2700
2899
  const finalStep = {
2701
2900
  terminate: true,
2702
2901
  step: 'Success',
2703
- message: `Processed ${conversations.length} conversations (${totalMessages} messages) and ${agentRuns.length} agent runs. Created ${notesCreated} notes and ${examplesCreated} examples.${maintenanceSummary}`,
2902
+ message: `Processed ${conversations.length} conversations (${totalMessages} messages) and ${agentRuns.length} agent runs. Created ${notesCreated} notes and ${examplesCreated} examples.${hardeningSummary}${maintenanceSummary}`,
2704
2903
  newPayload: {
2705
2904
  notesCreated,
2706
2905
  examplesCreated,