@framers/agentos 0.10.22 → 0.10.24

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 (204) hide show
  1. package/README.md +1 -1
  2. package/dist/api/AgentOS.d.ts.map +1 -1
  3. package/dist/api/AgentOS.js +3 -2
  4. package/dist/api/AgentOS.js.map +1 -1
  5. package/dist/api/agent.d.ts.map +1 -1
  6. package/dist/api/agent.js +25 -10
  7. package/dist/api/agent.js.map +1 -1
  8. package/dist/api/embedText.d.ts.map +1 -1
  9. package/dist/api/embedText.js +53 -17
  10. package/dist/api/embedText.js.map +1 -1
  11. package/dist/api/generateImage.d.ts.map +1 -1
  12. package/dist/api/generateImage.js +51 -7
  13. package/dist/api/generateImage.js.map +1 -1
  14. package/dist/api/generateText.d.ts +50 -7
  15. package/dist/api/generateText.d.ts.map +1 -1
  16. package/dist/api/generateText.js +265 -35
  17. package/dist/api/generateText.js.map +1 -1
  18. package/dist/api/model.d.ts +25 -1
  19. package/dist/api/model.d.ts.map +1 -1
  20. package/dist/api/model.js +108 -3
  21. package/dist/api/model.js.map +1 -1
  22. package/dist/api/runtime/SelfImprovementSessionManager.d.ts +34 -2
  23. package/dist/api/runtime/SelfImprovementSessionManager.d.ts.map +1 -1
  24. package/dist/api/runtime/SelfImprovementSessionManager.js +119 -33
  25. package/dist/api/runtime/SelfImprovementSessionManager.js.map +1 -1
  26. package/dist/api/runtime/global-default.d.ts +9 -1
  27. package/dist/api/runtime/global-default.d.ts.map +1 -1
  28. package/dist/api/runtime/global-default.js.map +1 -1
  29. package/dist/api/runtime/provider-defaults.d.ts.map +1 -1
  30. package/dist/api/runtime/provider-defaults.js +10 -4
  31. package/dist/api/runtime/provider-defaults.js.map +1 -1
  32. package/dist/api/runtime/tool-emulation/history.d.ts +16 -0
  33. package/dist/api/runtime/tool-emulation/history.d.ts.map +1 -0
  34. package/dist/api/runtime/tool-emulation/history.js +89 -0
  35. package/dist/api/runtime/tool-emulation/history.js.map +1 -0
  36. package/dist/api/runtime/tool-emulation/index.d.ts +1 -0
  37. package/dist/api/runtime/tool-emulation/index.d.ts.map +1 -1
  38. package/dist/api/runtime/tool-emulation/index.js +1 -0
  39. package/dist/api/runtime/tool-emulation/index.js.map +1 -1
  40. package/dist/api/runtime/tool-emulation/loop.d.ts +6 -0
  41. package/dist/api/runtime/tool-emulation/loop.d.ts.map +1 -1
  42. package/dist/api/runtime/tool-emulation/loop.js +1 -0
  43. package/dist/api/runtime/tool-emulation/loop.js.map +1 -1
  44. package/dist/api/sessionTranscript.d.ts +34 -0
  45. package/dist/api/sessionTranscript.d.ts.map +1 -1
  46. package/dist/api/sessionTranscript.js +61 -1
  47. package/dist/api/sessionTranscript.js.map +1 -1
  48. package/dist/api/streamText.d.ts.map +1 -1
  49. package/dist/api/streamText.js +120 -24
  50. package/dist/api/streamText.js.map +1 -1
  51. package/dist/api/types/AgentOSInput.d.ts +11 -0
  52. package/dist/api/types/AgentOSInput.d.ts.map +1 -1
  53. package/dist/api/types.d.ts +14 -6
  54. package/dist/api/types.d.ts.map +1 -1
  55. package/dist/api/types.js.map +1 -1
  56. package/dist/cognition/emergent/AdaptPersonalityTool.d.ts +13 -4
  57. package/dist/cognition/emergent/AdaptPersonalityTool.d.ts.map +1 -1
  58. package/dist/cognition/emergent/AdaptPersonalityTool.js +32 -23
  59. package/dist/cognition/emergent/AdaptPersonalityTool.js.map +1 -1
  60. package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts +18 -6
  61. package/dist/cognition/emergent/EmergentCapabilityEngine.d.ts.map +1 -1
  62. package/dist/cognition/emergent/EmergentCapabilityEngine.js.map +1 -1
  63. package/dist/cognition/emergent/SelfEvaluateTool.d.ts +10 -3
  64. package/dist/cognition/emergent/SelfEvaluateTool.d.ts.map +1 -1
  65. package/dist/cognition/emergent/SelfEvaluateTool.js +3 -2
  66. package/dist/cognition/emergent/SelfEvaluateTool.js.map +1 -1
  67. package/dist/cognition/substrate/CognitiveMemoryBridge.d.ts +51 -2
  68. package/dist/cognition/substrate/CognitiveMemoryBridge.d.ts.map +1 -1
  69. package/dist/cognition/substrate/CognitiveMemoryBridge.js +84 -2
  70. package/dist/cognition/substrate/CognitiveMemoryBridge.js.map +1 -1
  71. package/dist/cognition/substrate/ConversationHistoryManager.d.ts +8 -4
  72. package/dist/cognition/substrate/ConversationHistoryManager.d.ts.map +1 -1
  73. package/dist/cognition/substrate/ConversationHistoryManager.js +17 -5
  74. package/dist/cognition/substrate/ConversationHistoryManager.js.map +1 -1
  75. package/dist/cognition/substrate/GMI.d.ts +92 -5
  76. package/dist/cognition/substrate/GMI.d.ts.map +1 -1
  77. package/dist/cognition/substrate/GMI.js +291 -65
  78. package/dist/cognition/substrate/GMI.js.map +1 -1
  79. package/dist/cognition/substrate/GMIManager.d.ts +12 -0
  80. package/dist/cognition/substrate/GMIManager.d.ts.map +1 -1
  81. package/dist/cognition/substrate/GMIManager.js +22 -16
  82. package/dist/cognition/substrate/GMIManager.js.map +1 -1
  83. package/dist/cognition/substrate/IGMI.d.ts +23 -0
  84. package/dist/cognition/substrate/IGMI.d.ts.map +1 -1
  85. package/dist/cognition/substrate/IGMI.js.map +1 -1
  86. package/dist/cognition/substrate/MetapromptExecutor.d.ts +183 -28
  87. package/dist/cognition/substrate/MetapromptExecutor.d.ts.map +1 -1
  88. package/dist/cognition/substrate/MetapromptExecutor.js +431 -108
  89. package/dist/cognition/substrate/MetapromptExecutor.js.map +1 -1
  90. package/dist/cognition/substrate/personas/IPersonaDefinition.d.ts +5 -0
  91. package/dist/cognition/substrate/personas/IPersonaDefinition.d.ts.map +1 -1
  92. package/dist/cognition/substrate/personas/PersonaValidation.d.ts.map +1 -1
  93. package/dist/cognition/substrate/personas/PersonaValidation.js +30 -0
  94. package/dist/cognition/substrate/personas/PersonaValidation.js.map +1 -1
  95. package/dist/cognition/substrate/personas/SoulLoader.d.ts +14 -1
  96. package/dist/cognition/substrate/personas/SoulLoader.d.ts.map +1 -1
  97. package/dist/cognition/substrate/personas/SoulLoader.js +45 -12
  98. package/dist/cognition/substrate/personas/SoulLoader.js.map +1 -1
  99. package/dist/cognition/substrate/personas/definitions/default_assistant_persona.json +3 -35
  100. package/dist/cognition/substrate/personas/hexaco.d.ts +34 -0
  101. package/dist/cognition/substrate/personas/hexaco.d.ts.map +1 -0
  102. package/dist/cognition/substrate/personas/hexaco.js +66 -0
  103. package/dist/cognition/substrate/personas/hexaco.js.map +1 -0
  104. package/dist/cognition/substrate/userFeedback.d.ts +69 -0
  105. package/dist/cognition/substrate/userFeedback.d.ts.map +1 -0
  106. package/dist/cognition/substrate/userFeedback.js +107 -0
  107. package/dist/cognition/substrate/userFeedback.js.map +1 -0
  108. package/dist/core/conversation/ConversationMessage.d.ts +9 -0
  109. package/dist/core/conversation/ConversationMessage.d.ts.map +1 -1
  110. package/dist/core/conversation/ConversationMessage.js.map +1 -1
  111. package/dist/core/llm/IPromptEngine.d.ts +9 -0
  112. package/dist/core/llm/IPromptEngine.d.ts.map +1 -1
  113. package/dist/core/llm/IPromptEngine.js.map +1 -1
  114. package/dist/core/llm/PromptEngine.d.ts +30 -1
  115. package/dist/core/llm/PromptEngine.d.ts.map +1 -1
  116. package/dist/core/llm/PromptEngine.js +229 -92
  117. package/dist/core/llm/PromptEngine.js.map +1 -1
  118. package/dist/core/llm/providers/AIModelProviderManager.d.ts +20 -0
  119. package/dist/core/llm/providers/AIModelProviderManager.d.ts.map +1 -1
  120. package/dist/core/llm/providers/AIModelProviderManager.js +35 -5
  121. package/dist/core/llm/providers/AIModelProviderManager.js.map +1 -1
  122. package/dist/core/llm/providers/IProvider.d.ts +28 -8
  123. package/dist/core/llm/providers/IProvider.d.ts.map +1 -1
  124. package/dist/core/llm/providers/implementations/AnthropicProvider.d.ts +135 -49
  125. package/dist/core/llm/providers/implementations/AnthropicProvider.d.ts.map +1 -1
  126. package/dist/core/llm/providers/implementations/AnthropicProvider.js +323 -114
  127. package/dist/core/llm/providers/implementations/AnthropicProvider.js.map +1 -1
  128. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.d.ts.map +1 -1
  129. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.js +15 -1
  130. package/dist/core/llm/providers/implementations/ClaudeCodeProvider.js.map +1 -1
  131. package/dist/core/llm/providers/implementations/GeminiCLIProvider.d.ts +1 -1
  132. package/dist/core/llm/providers/implementations/GeminiCLIProvider.d.ts.map +1 -1
  133. package/dist/core/llm/providers/implementations/GeminiCLIProvider.js +51 -24
  134. package/dist/core/llm/providers/implementations/GeminiCLIProvider.js.map +1 -1
  135. package/dist/core/llm/providers/implementations/GeminiProvider.d.ts +25 -6
  136. package/dist/core/llm/providers/implementations/GeminiProvider.d.ts.map +1 -1
  137. package/dist/core/llm/providers/implementations/GeminiProvider.js +108 -90
  138. package/dist/core/llm/providers/implementations/GeminiProvider.js.map +1 -1
  139. package/dist/core/llm/providers/implementations/MistralProvider.d.ts +11 -0
  140. package/dist/core/llm/providers/implementations/MistralProvider.d.ts.map +1 -1
  141. package/dist/core/llm/providers/implementations/MistralProvider.js +32 -2
  142. package/dist/core/llm/providers/implementations/MistralProvider.js.map +1 -1
  143. package/dist/core/llm/providers/implementations/OllamaProvider.js +7 -7
  144. package/dist/core/llm/providers/implementations/OllamaProvider.js.map +1 -1
  145. package/dist/core/llm/providers/implementations/OpenAIProvider.d.ts +53 -2
  146. package/dist/core/llm/providers/implementations/OpenAIProvider.d.ts.map +1 -1
  147. package/dist/core/llm/providers/implementations/OpenAIProvider.js +84 -17
  148. package/dist/core/llm/providers/implementations/OpenAIProvider.js.map +1 -1
  149. package/dist/core/llm/providers/implementations/OpenRouterProvider.d.ts.map +1 -1
  150. package/dist/core/llm/providers/implementations/OpenRouterProvider.js +8 -3
  151. package/dist/core/llm/providers/implementations/OpenRouterProvider.js.map +1 -1
  152. package/dist/core/llm/providers/implementations/TogetherProvider.d.ts +3 -3
  153. package/dist/core/llm/providers/implementations/TogetherProvider.d.ts.map +1 -1
  154. package/dist/core/llm/providers/implementations/TogetherProvider.js +25 -13
  155. package/dist/core/llm/providers/implementations/TogetherProvider.js.map +1 -1
  156. package/dist/core/llm/providers/implementations/cacheLeakDetector.d.ts +15 -7
  157. package/dist/core/llm/providers/implementations/cacheLeakDetector.d.ts.map +1 -1
  158. package/dist/core/llm/providers/implementations/cacheLeakDetector.js +12 -7
  159. package/dist/core/llm/providers/implementations/cacheLeakDetector.js.map +1 -1
  160. package/dist/core/llm/providers/model-cache-capabilities.d.ts +24 -9
  161. package/dist/core/llm/providers/model-cache-capabilities.d.ts.map +1 -1
  162. package/dist/core/llm/providers/model-cache-capabilities.js +59 -44
  163. package/dist/core/llm/providers/model-cache-capabilities.js.map +1 -1
  164. package/dist/core/llm/providers/model-effort.d.ts +14 -0
  165. package/dist/core/llm/providers/model-effort.d.ts.map +1 -1
  166. package/dist/core/llm/providers/model-effort.js +22 -0
  167. package/dist/core/llm/providers/model-effort.js.map +1 -1
  168. package/dist/core/llm/providers/model-forced-tool-choice.d.ts +13 -6
  169. package/dist/core/llm/providers/model-forced-tool-choice.d.ts.map +1 -1
  170. package/dist/core/llm/providers/model-forced-tool-choice.js +14 -7
  171. package/dist/core/llm/providers/model-forced-tool-choice.js.map +1 -1
  172. package/dist/core/llm/providers/model-thinking.d.ts +64 -13
  173. package/dist/core/llm/providers/model-thinking.d.ts.map +1 -1
  174. package/dist/core/llm/providers/model-thinking.js +52 -12
  175. package/dist/core/llm/providers/model-thinking.js.map +1 -1
  176. package/dist/core/llm/providers/openrouter-only-params.d.ts +26 -7
  177. package/dist/core/llm/providers/openrouter-only-params.d.ts.map +1 -1
  178. package/dist/core/llm/providers/openrouter-only-params.js +58 -9
  179. package/dist/core/llm/providers/openrouter-only-params.js.map +1 -1
  180. package/dist/core/llm/providers/url-secrets.d.ts +42 -0
  181. package/dist/core/llm/providers/url-secrets.d.ts.map +1 -0
  182. package/dist/core/llm/providers/url-secrets.js +64 -0
  183. package/dist/core/llm/providers/url-secrets.js.map +1 -0
  184. package/dist/core/llm/streaming/StreamingBatcher.d.ts +4 -3
  185. package/dist/core/llm/streaming/StreamingBatcher.d.ts.map +1 -1
  186. package/dist/core/llm/streaming/StreamingBatcher.js +8 -3
  187. package/dist/core/llm/streaming/StreamingBatcher.js.map +1 -1
  188. package/dist/core/safety/LLMProviderHealthRegistry.d.ts +4 -0
  189. package/dist/core/safety/LLMProviderHealthRegistry.d.ts.map +1 -1
  190. package/dist/core/safety/LLMProviderHealthRegistry.js +26 -0
  191. package/dist/core/safety/LLMProviderHealthRegistry.js.map +1 -1
  192. package/dist/core/utils/sha256.d.ts +19 -0
  193. package/dist/core/utils/sha256.d.ts.map +1 -0
  194. package/dist/core/utils/sha256.js +101 -0
  195. package/dist/core/utils/sha256.js.map +1 -0
  196. package/dist/index.d.ts +1 -1
  197. package/dist/index.d.ts.map +1 -1
  198. package/dist/index.js +1 -1
  199. package/dist/index.js.map +1 -1
  200. package/dist/orchestration/pipeline/query/types.js +3 -3
  201. package/dist/orchestration/pipeline/query/types.js.map +1 -1
  202. package/dist/orchestration/planning/ProviderAssignmentEngine.js +2 -2
  203. package/dist/orchestration/planning/ProviderAssignmentEngine.js.map +1 -1
  204. package/package.json +1 -1
@@ -13,15 +13,42 @@
13
13
  *
14
14
  * @module cognitive_substrate/MetapromptExecutor
15
15
  */
16
- import { GMIMood, GMIPrimeState, } from './IGMI.js';
16
+ import { GMIMood, } from './IGMI.js';
17
17
  import { GMIEventType } from './GMIEvent.js';
18
18
  import { GMIError, GMIErrorCode, createGMIErrorFromError } from '../../core/utils/errors.js';
19
+ /** Longest time {@link MetapromptExecutor.drain} waits by default, in milliseconds. */
20
+ const DEFAULT_DRAIN_TIMEOUT_MS = 5000;
21
+ /**
22
+ * Longest a queued unit of metaprompt work may run before the queue moves on
23
+ * without it, in milliseconds. Each metaprompt LLM call also carries it as its
24
+ * request timeout, so a stalled provider call is normally cut off first.
25
+ */
26
+ const DEFAULT_RUN_TIMEOUT_MS = 120000;
27
+ /**
28
+ * Working-memory keys that hold the GMI's own state. A metaprompt changes
29
+ * mood, user and task context through its update fields, which the GMI
30
+ * persists under these keys; an imprint that wrote them would bypass that
31
+ * path (and race it after a deadline) or fire a manual trigger.
32
+ */
33
+ function isGmiStateKey(key) {
34
+ return (key === 'currentGmiMood' ||
35
+ key === 'currentUserContext' ||
36
+ key === 'currentTaskContext' ||
37
+ key === 'gmi_sentiment_history' ||
38
+ key.startsWith('manual_trigger_') ||
39
+ key.startsWith('metaprompt_turn_counter_'));
40
+ }
19
41
  /**
20
42
  * Handles metaprompt trigger checking, execution, and state application.
21
43
  *
22
- * Owns the `metaPromptTriggerCounters` map and the `selfReflectionIntervalTurns`
23
- * / `turnsSinceLastReflection` counters (though the latter are not currently
24
- * incremented by the executor itself — the GMI's turn loop still manages them).
44
+ * The metaprompts that fire on one turn are queued as one background batch.
45
+ * Batches and manual self-reflection cycles run one at a time, in the order
46
+ * they were queued, so two of them never race on the mood and context they
47
+ * update, and the turn that triggered a batch does not wait for its LLM calls.
48
+ * Metaprompt work never changes the GMI's lifecycle state; that state belongs
49
+ * to the turn loop.
50
+ *
51
+ * Owns the per-metaprompt turn counters used by `turn_interval` triggers.
25
52
  *
26
53
  * All GMI state mutations flow back through callbacks so the executor never
27
54
  * directly mutates GMI internals.
@@ -36,6 +63,29 @@ export class MetapromptExecutor {
36
63
  this.config = config;
37
64
  /** Per-metaprompt turn counters for `turn_interval` triggers. */
38
65
  this.metaPromptTriggerCounters = new Map();
66
+ /**
67
+ * Tail of the metaprompt work queue. Every batch and self-reflection cycle
68
+ * chains onto it, so only one runs at a time. It never rejects.
69
+ */
70
+ this.queueTail = Promise.resolve();
71
+ /** Number of batches and self-reflection cycles that are queued or running. */
72
+ this.inFlight = 0;
73
+ /** Metaprompt ids already reported as unable to fire, so each is reported once. */
74
+ this.invalidTriggerWarnings = new Set();
75
+ /** Set by {@link MetapromptExecutor.close}: no further work starts, and late results are discarded. */
76
+ this.closed = false;
77
+ /** User turns counted so far, to tell whether a queued event batch still belongs to the latest turn. */
78
+ this.countedTurns = 0;
79
+ /** Deadline for each queued unit of work, in milliseconds (see DEFAULT_RUN_TIMEOUT_MS). */
80
+ this.runTimeoutMs = DEFAULT_RUN_TIMEOUT_MS;
81
+ /**
82
+ * The last memory imprint write queued for each key. Writes to one key run
83
+ * in the order they were made: a unit that passes its deadline mid-write
84
+ * releases the queue, but a later unit's write to the same key waits for
85
+ * that write, so it is never overwritten by it. Writes to other keys do
86
+ * not wait, so one stalled write cannot hold back the rest.
87
+ */
88
+ this.writeTails = new Map();
39
89
  /** Turns elapsed since the last self-reflection. */
40
90
  this.turnsSinceLastReflection = 0;
41
91
  this.selfReflectionIntervalTurns = 5; // Will be overridden by initialize
@@ -44,62 +94,199 @@ export class MetapromptExecutor {
44
94
  // Public API
45
95
  // ---------------------------------------------------------------------------
46
96
  /**
47
- * Checks all metaprompt triggers and executes any that fire.
97
+ * Checks all metaprompt triggers and queues any that fire.
48
98
  *
49
99
  * Iterates through the persona's metaprompt definitions, evaluating each
50
100
  * trigger type:
51
- * - `turn_interval`: increments a counter and fires when the interval is reached.
101
+ * - `turn_interval`: counts user turns and fires on every `intervalTurns`-th
102
+ * one, so an interval of N fires on user turns N, 2N, 3N and so on.
52
103
  * - `event_based`: fires if the event type is in the pending events set.
53
104
  * - `manual`: fires if a flag was set in working memory.
54
105
  *
55
- * Triggered metaprompts are executed in parallel via `Promise.allSettled`.
56
- * Execution errors are logged but do not block the turn.
106
+ * The metaprompts that fire are queued as one background batch and this
107
+ * method returns without waiting for their LLM calls. Batches run one at a
108
+ * time; inside a batch the metaprompts run in parallel via
109
+ * `Promise.allSettled`. Execution errors are logged to the reasoning trace
110
+ * and never reach the turn.
57
111
  *
58
112
  * @param turnId - The current turn identifier (for tracing).
113
+ * @param options - `countTurn` says whether this turn counts toward
114
+ * `turn_interval` triggers. Pass `false` for turns that are not user
115
+ * messages, such as tool continuations and system turns. Defaults to `true`.
59
116
  */
60
- async checkAndTriggerMetaprompts(turnId) {
117
+ async checkAndTriggerMetaprompts(turnId, options = {}) {
61
118
  const persona = this.config.getPersona();
62
119
  if (!persona.metaPrompts || persona.metaPrompts.length === 0) {
63
120
  return;
64
121
  }
122
+ const countTurn = options.countTurn !== false;
123
+ if (countTurn)
124
+ this.countedTurns += 1;
125
+ const queuedAtTurn = this.countedTurns;
65
126
  const triggeredMetaPrompts = [];
66
127
  const pendingEvents = this.config.getPendingEvents();
67
128
  for (const metaPrompt of persona.metaPrompts) {
68
- if (!metaPrompt.trigger)
129
+ const trigger = metaPrompt.trigger;
130
+ if (!trigger)
69
131
  continue;
70
- if (metaPrompt.trigger.type === 'turn_interval') {
71
- const counter = await this.getMetapromptTurnCounter(metaPrompt.id);
72
- if (counter >= metaPrompt.trigger.intervalTurns) {
73
- triggeredMetaPrompts.push(metaPrompt);
74
- await this.resetMetapromptTurnCounter(metaPrompt.id);
132
+ switch (trigger.type) {
133
+ case 'turn_interval':
134
+ if (countTurn && (await this.advanceTurnInterval(metaPrompt, trigger.intervalTurns))) {
135
+ triggeredMetaPrompts.push(metaPrompt);
136
+ }
137
+ break;
138
+ case 'event_based': {
139
+ const eventName = trigger.eventName;
140
+ if (pendingEvents.has(eventName)) {
141
+ triggeredMetaPrompts.push(metaPrompt);
142
+ pendingEvents.delete(eventName);
143
+ }
144
+ break;
75
145
  }
76
- else {
77
- await this.incrementMetapromptTurnCounter(metaPrompt.id);
146
+ case 'manual': {
147
+ const manualFlag = await this.config.workingMemory.get(`manual_trigger_${metaPrompt.id}`);
148
+ if (manualFlag) {
149
+ triggeredMetaPrompts.push(metaPrompt);
150
+ await this.config.workingMemory.delete(`manual_trigger_${metaPrompt.id}`);
151
+ }
152
+ break;
78
153
  }
79
- }
80
- else if (metaPrompt.trigger.type === 'event_based') {
81
- const eventName = metaPrompt.trigger.eventName;
82
- if (pendingEvents.has(eventName)) {
83
- triggeredMetaPrompts.push(metaPrompt);
84
- pendingEvents.delete(eventName);
85
- }
86
- }
87
- else if (metaPrompt.trigger.type === 'manual') {
88
- const manualFlag = await this.config.workingMemory.get(`manual_trigger_${metaPrompt.id}`);
89
- if (manualFlag) {
90
- triggeredMetaPrompts.push(metaPrompt);
91
- await this.config.workingMemory.delete(`manual_trigger_${metaPrompt.id}`);
154
+ default: {
155
+ // Persona JSON is cast rather than type-checked, so other trigger
156
+ // types reach this point at runtime. Nothing ever fires them.
157
+ const unsupportedType = String(trigger.type);
158
+ this.warnInvalidTrigger(metaPrompt, `trigger type '${unsupportedType}' is not supported (expected turn_interval, event_based or manual)`);
92
159
  }
93
160
  }
94
161
  }
95
162
  if (triggeredMetaPrompts.length > 0) {
96
163
  this.config.addTraceEntry('SELF_REFLECTION_TRIGGERED', `${triggeredMetaPrompts.length} metaprompt(s) triggered`, { ids: triggeredMetaPrompts.map((m) => m.id), turnId });
97
- this.executeMetaprompts(triggeredMetaPrompts).catch((err) => {
98
- console.error(`GMI (ID: ${this.config.getGmiId()}): Metaprompt execution error:`, err);
99
- this.config.addTraceEntry('ERROR', 'Metaprompt execution failed', { error: err.message });
164
+ // Queue the batch and return: the turn does not wait for metaprompt LLM calls.
165
+ void this.enqueue('Metaprompt execution', (run) => {
166
+ // An event-based metaprompt reacts to the turn that raised the event
167
+ // (a frustrated or confused message). If a newer user turn has been
168
+ // counted while this batch waited behind a slow one, that evidence is
169
+ // gone, so it is dropped rather than applied to a later mood.
170
+ const current = this.countedTurns === queuedAtTurn;
171
+ const runnable = current
172
+ ? triggeredMetaPrompts
173
+ : triggeredMetaPrompts.filter((mp) => mp.trigger?.type !== 'event_based');
174
+ if (runnable.length < triggeredMetaPrompts.length) {
175
+ this.config.addTraceEntry('DEBUG', 'Skipped event-based metaprompts whose triggering turn has passed.', {
176
+ ids: triggeredMetaPrompts.filter((mp) => !runnable.includes(mp)).map((mp) => mp.id),
177
+ });
178
+ }
179
+ return this.executeMetaprompts(runnable, run);
100
180
  });
101
181
  }
102
182
  }
183
+ /**
184
+ * Reports whether a metaprompt batch or self-reflection cycle is queued or running.
185
+ *
186
+ * @returns `true` while any metaprompt work is pending.
187
+ */
188
+ isRunning() {
189
+ // A unit released at its deadline can leave a memory write running.
190
+ return this.inFlight > 0 || this.writeTails.size > 0;
191
+ }
192
+ /**
193
+ * Resolves once every queued metaprompt batch and self-reflection cycle has
194
+ * settled, including work queued while waiting. Never rejects.
195
+ */
196
+ async whenIdle() {
197
+ let tail;
198
+ do {
199
+ tail = this.queueTail;
200
+ await tail;
201
+ // Memory writes a unit started before its deadline outlive the unit.
202
+ await Promise.all(this.writeTails.values());
203
+ } while (tail !== this.queueTail || this.writeTails.size > 0);
204
+ }
205
+ /**
206
+ * Waits for queued and running metaprompt work to settle, for at most
207
+ * `timeoutMs`. The GMI calls this during shutdown so that a batch still in
208
+ * flight can store its updates before working memory closes, while a
209
+ * provider call that never settles cannot hold shutdown open.
210
+ *
211
+ * @param timeoutMs - Longest wait, in milliseconds.
212
+ * @returns `true` when all work settled, `false` when the wait timed out.
213
+ */
214
+ async drain(timeoutMs = DEFAULT_DRAIN_TIMEOUT_MS) {
215
+ if (!this.isRunning())
216
+ return true;
217
+ let resolveTimedOut;
218
+ const timedOut = new Promise((resolve) => {
219
+ resolveTimedOut = resolve;
220
+ });
221
+ const timer = setTimeout(() => resolveTimedOut(false), timeoutMs);
222
+ // Waiting on a hung provider call must not keep the process alive.
223
+ timer.unref?.();
224
+ try {
225
+ return await Promise.race([this.whenIdle().then(() => true), timedOut]);
226
+ }
227
+ finally {
228
+ clearTimeout(timer);
229
+ }
230
+ }
231
+ /**
232
+ * Stops the queue for good, as the GMI shuts down: work that has not started
233
+ * is skipped, and results of work still running are discarded instead of
234
+ * applied to memories that are closing.
235
+ */
236
+ close() {
237
+ this.closed = true;
238
+ }
239
+ /**
240
+ * Chains one unit of metaprompt work onto the queue, so it starts only after
241
+ * all earlier work has settled. A failure is logged to the console and the
242
+ * reasoning trace, and the queue moves on to the next unit.
243
+ *
244
+ * @param label - Name used in failure logs (for example 'Metaprompt execution').
245
+ * @param task - The work to run.
246
+ * @returns A promise that settles when this unit finishes. It never rejects.
247
+ */
248
+ enqueue(label, task) {
249
+ this.inFlight += 1;
250
+ let overdue = false;
251
+ const run = { isStale: () => overdue || this.closed };
252
+ const unit = async () => {
253
+ if (this.closed)
254
+ return;
255
+ // A provider call that never settles would hold the queue forever;
256
+ // past the deadline the queue moves on, and the unit's late results
257
+ // are discarded (run.isStale()).
258
+ const timeoutMs = this.runTimeoutMs;
259
+ let timer;
260
+ const deadline = new Promise((resolve) => {
261
+ timer = setTimeout(() => resolve('timeout'), timeoutMs);
262
+ timer.unref?.();
263
+ });
264
+ try {
265
+ const outcome = await Promise.race([task(run).then(() => 'done'), deadline]);
266
+ if (outcome === 'timeout') {
267
+ overdue = true;
268
+ this.config.addTraceEntry('WARNING', `${label} did not finish within ${timeoutMs}ms; its results will be discarded.`);
269
+ }
270
+ }
271
+ finally {
272
+ if (timer)
273
+ clearTimeout(timer);
274
+ }
275
+ };
276
+ const settled = this.queueTail
277
+ .then(unit)
278
+ .catch((error) => {
279
+ console.error(`GMI (ID: ${this.config.getGmiId()}): ${label} failed:`, error);
280
+ this.config.addTraceEntry('ERROR', `${label} failed`, {
281
+ error: error instanceof Error ? error.message : String(error),
282
+ });
283
+ })
284
+ .finally(() => {
285
+ this.inFlight -= 1;
286
+ });
287
+ this.queueTail = settled;
288
+ return settled;
289
+ }
103
290
  /**
104
291
  * Gets the turn counter for a specific metaprompt.
105
292
  *
@@ -124,9 +311,7 @@ export class MetapromptExecutor {
124
311
  */
125
312
  async incrementMetapromptTurnCounter(metapromptId) {
126
313
  const current = await this.getMetapromptTurnCounter(metapromptId);
127
- const newValue = current + 1;
128
- this.metaPromptTriggerCounters.set(metapromptId, newValue);
129
- await this.config.workingMemory.set(`metaprompt_turn_counter_${metapromptId}`, newValue);
314
+ await this.setMetapromptTurnCounter(metapromptId, current + 1);
130
315
  }
131
316
  /**
132
317
  * Resets the turn counter for a specific metaprompt to zero.
@@ -134,25 +319,85 @@ export class MetapromptExecutor {
134
319
  * @param metapromptId - The metaprompt identifier.
135
320
  */
136
321
  async resetMetapromptTurnCounter(metapromptId) {
137
- this.metaPromptTriggerCounters.set(metapromptId, 0);
138
- await this.config.workingMemory.set(`metaprompt_turn_counter_${metapromptId}`, 0);
322
+ await this.setMetapromptTurnCounter(metapromptId, 0);
139
323
  }
140
324
  /**
141
- * Executes multiple metaprompts in parallel using `Promise.allSettled`.
325
+ * Sets the turn counter for a specific metaprompt, in memory and in working
326
+ * memory, where it survives across GMI instances.
142
327
  *
143
- * Transitions the GMI to REFLECTING state during execution, restoring the
144
- * previous state afterwards.
328
+ * @param metapromptId - The metaprompt identifier.
329
+ * @param value - The number of counted user turns since the metaprompt last fired.
330
+ */
331
+ async setMetapromptTurnCounter(metapromptId, value) {
332
+ this.metaPromptTriggerCounters.set(metapromptId, value);
333
+ await this.config.workingMemory.set(`metaprompt_turn_counter_${metapromptId}`, value);
334
+ }
335
+ /**
336
+ * Counts one user turn toward a `turn_interval` metaprompt and reports
337
+ * whether the metaprompt fires on it.
338
+ *
339
+ * The counter holds the user turns counted since the metaprompt last fired.
340
+ * It is incremented first, so an interval of N fires on user turns N, 2N,
341
+ * 3N and so on, and it resets to zero when the metaprompt fires. A counter
342
+ * already above the interval fires on the next counted turn.
343
+ *
344
+ * Persona JSON is not type-checked, so the interval is validated here: a
345
+ * value that is not a finite number of at least 1 never fires and is
346
+ * reported once through `warnInvalidTrigger`.
347
+ *
348
+ * @param metaPrompt - The metaprompt whose counter advances.
349
+ * @param intervalTurns - The trigger's `intervalTurns`, as read from the persona.
350
+ * @returns `true` when the metaprompt fires on this turn.
351
+ */
352
+ async advanceTurnInterval(metaPrompt, intervalTurns) {
353
+ if (typeof intervalTurns !== 'number' || !Number.isFinite(intervalTurns) || intervalTurns < 1) {
354
+ this.warnInvalidTrigger(metaPrompt, `intervalTurns must be a number >= 1 (got ${String(intervalTurns)})`);
355
+ return false;
356
+ }
357
+ const counted = (await this.getMetapromptTurnCounter(metaPrompt.id)) + 1;
358
+ if (counted >= Math.floor(intervalTurns)) {
359
+ await this.resetMetapromptTurnCounter(metaPrompt.id);
360
+ return true;
361
+ }
362
+ await this.setMetapromptTurnCounter(metaPrompt.id, counted);
363
+ return false;
364
+ }
365
+ /**
366
+ * Reports a metaprompt whose trigger can never fire. Each metaprompt id is
367
+ * reported once per executor, with one WARNING trace entry and one console
368
+ * warning, so a misconfigured persona does not flood the trace every turn.
369
+ *
370
+ * @param metaPrompt - The metaprompt that will not run.
371
+ * @param reason - Why its trigger cannot fire.
372
+ */
373
+ warnInvalidTrigger(metaPrompt, reason) {
374
+ if (this.invalidTriggerWarnings.has(metaPrompt.id))
375
+ return;
376
+ this.invalidTriggerWarnings.add(metaPrompt.id);
377
+ const message = `Metaprompt '${metaPrompt.id}' will not run: ${reason}.`;
378
+ console.warn(`GMI (ID: ${this.config.getGmiId()}): ${message}`);
379
+ this.config.addTraceEntry('WARNING', message, {
380
+ metapromptId: metaPrompt.id,
381
+ trigger: metaPrompt.trigger,
382
+ });
383
+ }
384
+ /**
385
+ * Executes one batch of metaprompts in parallel using `Promise.allSettled`.
386
+ *
387
+ * A failed metaprompt is recorded as an ERROR trace entry and does not stop
388
+ * the others. The batch never changes the GMI's lifecycle state: it runs in
389
+ * the background, so the turn that triggered it may have finished and a
390
+ * later turn may be in progress. {@link MetapromptExecutor.checkAndTriggerMetaprompts}
391
+ * queues batches so that they run one at a time.
145
392
  *
146
393
  * @param metaPrompts - The metaprompt definitions to execute.
147
394
  */
148
- async executeMetaprompts(metaPrompts) {
395
+ async executeMetaprompts(metaPrompts, run) {
149
396
  if (metaPrompts.length === 0)
150
397
  return;
151
- const previousState = this.config.getState();
152
- this.config.setState(GMIPrimeState.REFLECTING);
153
398
  this.config.addTraceEntry('SELF_REFLECTION_START', `Executing ${metaPrompts.length} metaprompt(s)`, { ids: metaPrompts.map((m) => m.id) });
154
399
  try {
155
- const results = await Promise.allSettled(metaPrompts.map((mp) => this.executeMetapromptHandler(mp)));
400
+ const results = await Promise.allSettled(metaPrompts.map((mp) => this.executeMetapromptHandler(mp, run)));
156
401
  results.forEach((result, idx) => {
157
402
  if (result.status === 'rejected') {
158
403
  this.config.addTraceEntry('ERROR', `Metaprompt '${metaPrompts[idx].id}' failed: ${result.reason}`, { error: result.reason });
@@ -164,11 +409,6 @@ export class MetapromptExecutor {
164
409
  this.config.addTraceEntry('ERROR', gmiError.message, gmiError.toPlainObject());
165
410
  }
166
411
  finally {
167
- const disallowedStates = new Set([
168
- GMIPrimeState.IDLE,
169
- GMIPrimeState.INITIALIZING,
170
- ]);
171
- this.config.setState(disallowedStates.has(previousState) ? GMIPrimeState.READY : previousState);
172
412
  this.config.addTraceEntry('SELF_REFLECTION_COMPLETE', 'Metaprompt execution cycle complete');
173
413
  }
174
414
  }
@@ -180,22 +420,22 @@ export class MetapromptExecutor {
180
420
  *
181
421
  * @param metaPrompt - The metaprompt definition to execute.
182
422
  */
183
- async executeMetapromptHandler(metaPrompt) {
423
+ async executeMetapromptHandler(metaPrompt, run) {
184
424
  switch (metaPrompt.id) {
185
425
  case 'gmi_self_trait_adjustment':
186
- return this.handleTraitAdjustment(metaPrompt);
426
+ return this.handleTraitAdjustment(metaPrompt, run);
187
427
  case 'gmi_frustration_recovery':
188
- return this.handleFrustrationRecovery(metaPrompt);
428
+ return this.handleFrustrationRecovery(metaPrompt, run);
189
429
  case 'gmi_confusion_clarification':
190
- return this.handleConfusionClarification(metaPrompt);
430
+ return this.handleConfusionClarification(metaPrompt, run);
191
431
  case 'gmi_satisfaction_reinforcement':
192
- return this.handleSatisfactionReinforcement(metaPrompt);
432
+ return this.handleSatisfactionReinforcement(metaPrompt, run);
193
433
  case 'gmi_error_recovery':
194
- return this.handleErrorRecovery(metaPrompt);
434
+ return this.handleErrorRecovery(metaPrompt, run);
195
435
  case 'gmi_engagement_boost':
196
- return this.handleEngagementBoost(metaPrompt);
436
+ return this.handleEngagementBoost(metaPrompt, run);
197
437
  default:
198
- return this.handleGenericMetaprompt(metaPrompt);
438
+ return this.handleGenericMetaprompt(metaPrompt, run);
199
439
  }
200
440
  }
201
441
  /**
@@ -206,7 +446,7 @@ export class MetapromptExecutor {
206
446
  *
207
447
  * @param metaPrompt - The metaprompt definition.
208
448
  */
209
- async handleTraitAdjustment(metaPrompt) {
449
+ async handleTraitAdjustment(metaPrompt, run) {
210
450
  const evidenceHistory = this.config.getConversationHistory().slice(-10);
211
451
  const evidenceTrace = this.config.getReasoningTraceEntries().slice(-20);
212
452
  const evidence = {
@@ -222,15 +462,15 @@ export class MetapromptExecutor {
222
462
  user_skill: this.config.getUserContext().skillLevel || 'unknown',
223
463
  task_complexity: this.config.getTaskContext().complexity || 'unknown',
224
464
  };
225
- const response = await this.executeMetapromptWithVariables(metaPrompt, variables);
226
- await this.applyMetapromptUpdates(response, metaPrompt.id);
465
+ const response = await this.executeMetapromptWithVariables(metaPrompt, variables, run);
466
+ await this.applyMetapromptUpdates(response, metaPrompt.id, run);
227
467
  }
228
468
  /**
229
469
  * Handler for the frustration recovery metaprompt.
230
470
  *
231
471
  * @param metaPrompt - The metaprompt definition.
232
472
  */
233
- async handleFrustrationRecovery(metaPrompt) {
473
+ async handleFrustrationRecovery(metaPrompt, run) {
234
474
  const sentimentHistory = await this.config.workingMemory.get('gmi_sentiment_history');
235
475
  const recentErrors = this.config.getReasoningTraceEntries()
236
476
  .slice(-10)
@@ -245,15 +485,15 @@ export class MetapromptExecutor {
245
485
  user_skill: this.config.getUserContext().skillLevel || 'unknown',
246
486
  task_complexity: this.config.getTaskContext().complexity || 'unknown',
247
487
  };
248
- const response = await this.executeMetapromptWithVariables(metaPrompt, variables);
249
- await this.applyMetapromptUpdates(response, metaPrompt.id);
488
+ const response = await this.executeMetapromptWithVariables(metaPrompt, variables, run);
489
+ await this.applyMetapromptUpdates(response, metaPrompt.id, run);
250
490
  }
251
491
  /**
252
492
  * Handler for the confusion clarification metaprompt.
253
493
  *
254
494
  * @param metaPrompt - The metaprompt definition.
255
495
  */
256
- async handleConfusionClarification(metaPrompt) {
496
+ async handleConfusionClarification(metaPrompt, run) {
257
497
  const sentimentHistory = await this.config.workingMemory.get('gmi_sentiment_history');
258
498
  const lastConfusionEvent = this.config.getEventHistory()
259
499
  .slice()
@@ -270,15 +510,15 @@ export class MetapromptExecutor {
270
510
  user_skill: this.config.getUserContext().skillLevel || 'unknown',
271
511
  task_complexity: this.config.getTaskContext().complexity || 'unknown',
272
512
  };
273
- const response = await this.executeMetapromptWithVariables(metaPrompt, variables);
274
- await this.applyMetapromptUpdates(response, metaPrompt.id);
513
+ const response = await this.executeMetapromptWithVariables(metaPrompt, variables, run);
514
+ await this.applyMetapromptUpdates(response, metaPrompt.id, run);
275
515
  }
276
516
  /**
277
517
  * Handler for the satisfaction reinforcement metaprompt.
278
518
  *
279
519
  * @param metaPrompt - The metaprompt definition.
280
520
  */
281
- async handleSatisfactionReinforcement(metaPrompt) {
521
+ async handleSatisfactionReinforcement(metaPrompt, run) {
282
522
  const sentimentHistory = await this.config.workingMemory.get('gmi_sentiment_history');
283
523
  const variables = {
284
524
  current_sentiment: this.config.getUserContext().currentSentiment || 'positive',
@@ -289,15 +529,15 @@ export class MetapromptExecutor {
289
529
  user_skill: this.config.getUserContext().skillLevel || 'unknown',
290
530
  task_complexity: this.config.getTaskContext().complexity || 'unknown',
291
531
  };
292
- const response = await this.executeMetapromptWithVariables(metaPrompt, variables);
293
- await this.applyMetapromptUpdates(response, metaPrompt.id);
532
+ const response = await this.executeMetapromptWithVariables(metaPrompt, variables, run);
533
+ await this.applyMetapromptUpdates(response, metaPrompt.id, run);
294
534
  }
295
535
  /**
296
536
  * Handler for the error recovery metaprompt.
297
537
  *
298
538
  * @param metaPrompt - The metaprompt definition.
299
539
  */
300
- async handleErrorRecovery(metaPrompt) {
540
+ async handleErrorRecovery(metaPrompt, run) {
301
541
  const recentErrors = this.config.getReasoningTraceEntries()
302
542
  .slice(-10)
303
543
  .filter((e) => e.type === 'ERROR');
@@ -308,15 +548,15 @@ export class MetapromptExecutor {
308
548
  user_skill: this.config.getUserContext().skillLevel || 'unknown',
309
549
  task_complexity: this.config.getTaskContext().complexity || 'unknown',
310
550
  };
311
- const response = await this.executeMetapromptWithVariables(metaPrompt, variables);
312
- await this.applyMetapromptUpdates(response, metaPrompt.id);
551
+ const response = await this.executeMetapromptWithVariables(metaPrompt, variables, run);
552
+ await this.applyMetapromptUpdates(response, metaPrompt.id, run);
313
553
  }
314
554
  /**
315
555
  * Handler for the engagement boost metaprompt.
316
556
  *
317
557
  * @param metaPrompt - The metaprompt definition.
318
558
  */
319
- async handleEngagementBoost(metaPrompt) {
559
+ async handleEngagementBoost(metaPrompt, run) {
320
560
  const sentimentHistory = await this.config.workingMemory.get('gmi_sentiment_history');
321
561
  const variables = {
322
562
  consecutive_neutral: (sentimentHistory?.consecutiveConfusion || 4).toString(),
@@ -325,8 +565,8 @@ export class MetapromptExecutor {
325
565
  user_skill: this.config.getUserContext().skillLevel || 'unknown',
326
566
  task_complexity: this.config.getTaskContext().complexity || 'unknown',
327
567
  };
328
- const response = await this.executeMetapromptWithVariables(metaPrompt, variables);
329
- await this.applyMetapromptUpdates(response, metaPrompt.id);
568
+ const response = await this.executeMetapromptWithVariables(metaPrompt, variables, run);
569
+ await this.applyMetapromptUpdates(response, metaPrompt.id, run);
330
570
  }
331
571
  /**
332
572
  * Generic handler for custom metaprompts that don't have a dedicated handler.
@@ -335,7 +575,7 @@ export class MetapromptExecutor {
335
575
  *
336
576
  * @param metaPrompt - The metaprompt definition.
337
577
  */
338
- async handleGenericMetaprompt(metaPrompt) {
578
+ async handleGenericMetaprompt(metaPrompt, run) {
339
579
  const variables = {
340
580
  recent_conversation: JSON.stringify(this.config.getConversationHistory().slice(-5)),
341
581
  recent_reasoning: JSON.stringify(this.config.getReasoningTraceEntries().slice(-10)),
@@ -344,22 +584,24 @@ export class MetapromptExecutor {
344
584
  task_complexity: this.config.getTaskContext().complexity || 'unknown',
345
585
  current_sentiment: this.config.getUserContext().currentSentiment || 'neutral',
346
586
  };
347
- const response = await this.executeMetapromptWithVariables(metaPrompt, variables);
348
- await this.applyMetapromptUpdates(response, metaPrompt.id);
587
+ const response = await this.executeMetapromptWithVariables(metaPrompt, variables, run);
588
+ await this.applyMetapromptUpdates(response, metaPrompt.id, run);
349
589
  }
350
590
  /**
351
591
  * Executes a metaprompt template with variable substitution and LLM call.
352
592
  *
353
593
  * 1. Extracts the template string from the metaprompt definition.
354
594
  * 2. Substitutes `{{variable}}` placeholders with provided values.
355
- * 3. Calls the LLM with JSON response format.
356
- * 4. Parses the JSON response with LLM-based recovery via IUtilityAI.
595
+ * 3. Resolves the model and provider: the metaprompt's own, then the
596
+ * persona defaults, then the GMI defaults.
597
+ * 4. Calls the LLM with JSON response format.
598
+ * 5. Parses the JSON response with LLM-based recovery via IUtilityAI.
357
599
  *
358
600
  * @param metaPrompt - The metaprompt definition.
359
601
  * @param variables - Key-value pairs to substitute into the template.
360
602
  * @returns The parsed JSON response from the LLM.
361
603
  */
362
- async executeMetapromptWithVariables(metaPrompt, variables) {
604
+ async executeMetapromptWithVariables(metaPrompt, variables, run) {
363
605
  let template;
364
606
  if (typeof metaPrompt.promptTemplate === 'string') {
365
607
  template = metaPrompt.promptTemplate;
@@ -371,27 +613,39 @@ export class MetapromptExecutor {
371
613
  for (const [key, value] of Object.entries(variables)) {
372
614
  finalPrompt = finalPrompt.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
373
615
  }
374
- const persona = this.config.getPersona();
375
- const modelId = metaPrompt.modelId || persona.defaultModelId;
376
- const providerId = metaPrompt.providerId || persona.defaultProviderId;
377
- if (!modelId || !providerId) {
378
- throw new GMIError('No model or provider specified for metaprompt', GMIErrorCode.CONFIGURATION_ERROR);
379
- }
616
+ // Same resolution as the self-reflection path: the metaprompt's own model
617
+ // and provider, then the persona defaults, then the GMI's configured
618
+ // defaults. Throws a CONFIGURATION_ERROR GMIError when nothing resolves.
619
+ const { modelId, providerId } = this.config.getModelAndProvider(metaPrompt.modelId, metaPrompt.providerId);
380
620
  this.config.addTraceEntry('DEBUG', `Executing metaprompt '${metaPrompt.id}'`, { modelId, providerId });
381
621
  const completionOptions = {
382
622
  temperature: metaPrompt.temperature ?? 0.3,
383
623
  maxTokens: metaPrompt.maxOutputTokens ?? 512,
384
624
  responseFormat: { type: 'json_object' },
625
+ // Bounds a stalled provider call before the queue's own deadline does.
626
+ requestTimeout: this.runTimeoutMs,
385
627
  };
386
628
  const provider = this.config.llmProviderManager.getProvider(providerId);
387
629
  if (!provider) {
388
630
  throw new GMIError(`Provider '${providerId}' not found for metaprompt '${metaPrompt.id}'.`, GMIErrorCode.LLM_PROVIDER_UNAVAILABLE);
389
631
  }
632
+ // A run can go stale while its handler awaits memory reads; it then
633
+ // starts no provider call whose answer would be discarded.
634
+ if (run?.isStale()) {
635
+ this.config.addTraceEntry('WARNING', `Discarded late results of metaprompt '${metaPrompt.id}'.`);
636
+ return null;
637
+ }
390
638
  const result = await provider.generateCompletion(modelId, [{ role: 'user', content: finalPrompt }], completionOptions);
391
639
  const responseContent = result.choices?.[0]?.message?.content;
392
640
  if (!responseContent || typeof responseContent !== 'string') {
393
641
  throw new GMIError(`Metaprompt '${metaPrompt.id}' returned no valid content.`, GMIErrorCode.LLM_PROVIDER_ERROR, { response: result });
394
642
  }
643
+ // A run that went stale while the provider answered is discarded anyway,
644
+ // so no JSON repair call is made on its behalf.
645
+ if (run?.isStale()) {
646
+ this.config.addTraceEntry('WARNING', `Discarded late results of metaprompt '${metaPrompt.id}'.`);
647
+ return null;
648
+ }
395
649
  const parseOptions = {
396
650
  attemptFixWithLLM: true,
397
651
  llmModelIdForFix: modelId,
@@ -408,10 +662,20 @@ export class MetapromptExecutor {
408
662
  *
409
663
  * @param updates - The parsed updates from the metaprompt LLM response.
410
664
  * @param metapromptId - The ID of the metaprompt that produced these updates.
665
+ * @param run - The queued unit applying them; once it is stale, nothing
666
+ * more is applied.
667
+ * @returns Keys of the memory imprints stored.
411
668
  */
412
- async applyMetapromptUpdates(updates, metapromptId) {
669
+ async applyMetapromptUpdates(updates, metapromptId, run) {
670
+ const storedImprintKeys = [];
413
671
  if (!updates)
414
- return;
672
+ return storedImprintKeys;
673
+ if (run?.isStale()) {
674
+ // The queue moved on without this unit, or the GMI shut down: its
675
+ // results would land on state that no longer matches its evidence.
676
+ this.config.addTraceEntry('WARNING', `Discarded late results of metaprompt '${metapromptId}'.`);
677
+ return storedImprintKeys;
678
+ }
415
679
  let stateChanged = false;
416
680
  // Mood update — GMIMood enum values are lowercase (e.g., 'focused'),
417
681
  // but LLM responses may return either case. Normalize to lowercase for comparison.
@@ -440,13 +704,27 @@ export class MetapromptExecutor {
440
704
  // Memory imprints
441
705
  if (updates.newMemoryImprints && Array.isArray(updates.newMemoryImprints)) {
442
706
  for (const imprint of updates.newMemoryImprints) {
707
+ // A write can outlast the run's deadline. Once the queue has moved
708
+ // on or the GMI has shut down, the remaining imprints would overwrite
709
+ // newer values or reach a closed memory, so they are dropped.
710
+ if (run?.isStale()) {
711
+ this.config.addTraceEntry('WARNING', `Discarded late results of metaprompt '${metapromptId}'.`);
712
+ break;
713
+ }
714
+ if (imprint.key && isGmiStateKey(imprint.key)) {
715
+ this.config.addTraceEntry('WARNING', `Skipped memory imprint '${imprint.key}' from metaprompt '${metapromptId}': the key holds GMI state.`);
716
+ continue;
717
+ }
443
718
  if (imprint.key) {
444
- await this.config.workingMemory.set(imprint.key, imprint.value);
719
+ const stored = await this.writeInOrder(run, imprint.key, () => this.config.workingMemory.set(imprint.key, imprint.value));
720
+ if (!stored) {
721
+ this.config.addTraceEntry('WARNING', `Discarded late results of metaprompt '${metapromptId}'.`);
722
+ break;
723
+ }
724
+ storedImprintKeys.push(imprint.key);
725
+ stateChanged = true;
445
726
  }
446
727
  }
447
- if (updates.newMemoryImprints.length > 0) {
448
- stateChanged = true;
449
- }
450
728
  }
451
729
  // Log state change
452
730
  if (stateChanged) {
@@ -457,13 +735,43 @@ export class MetapromptExecutor {
457
735
  rationale: updates.adjustmentRationale || updates.recoveryStrategy || updates.clarificationStrategy || updates.engagementStrategy || updates.mitigationStrategy,
458
736
  });
459
737
  }
738
+ return storedImprintKeys;
739
+ }
740
+ /**
741
+ * Runs one memory write after every write to the same key queued before
742
+ * it, unless `run` has gone stale by the time its turn comes.
743
+ *
744
+ * @param run The unit making the write.
745
+ * @param key The working-memory key written.
746
+ * @param write The write itself.
747
+ * @returns Whether the write ran.
748
+ */
749
+ writeInOrder(run, key, write) {
750
+ const previous = this.writeTails.get(key) ?? Promise.resolve();
751
+ const result = previous.then(async () => {
752
+ if (run?.isStale())
753
+ return false;
754
+ await write();
755
+ return true;
756
+ });
757
+ const tail = result.then(() => undefined, () => undefined);
758
+ this.writeTails.set(key, tail);
759
+ // Forget the key once its last queued write settles.
760
+ void tail.then(() => {
761
+ if (this.writeTails.get(key) === tail)
762
+ this.writeTails.delete(key);
763
+ });
764
+ return result;
460
765
  }
461
766
  /**
462
767
  * Triggers and processes a full self-reflection cycle using the
463
768
  * `gmi_self_trait_adjustment` metaprompt.
464
769
  *
465
- * Performs the same work as `handleTraitAdjustment` plus guard checks
466
- * (already reflecting, no metaprompt defined) and explicit state management.
770
+ * Performs the same work as `handleTraitAdjustment`, behind two guards: the
771
+ * cycle is skipped when the persona defines no such metaprompt, and when a
772
+ * metaprompt batch or another cycle is already queued or running. The cycle
773
+ * runs on the metaprompt queue and never changes the GMI's lifecycle state.
774
+ * The returned promise settles when the cycle has finished.
467
775
  */
468
776
  async triggerAndProcessSelfReflection() {
469
777
  const persona = this.config.getPersona();
@@ -472,12 +780,20 @@ export class MetapromptExecutor {
472
780
  this.config.addTraceEntry('SELF_REFLECTION_SKIPPED', "Self-reflection disabled or no 'gmi_self_trait_adjustment' meta-prompt.");
473
781
  return;
474
782
  }
475
- if (this.config.getState() === GMIPrimeState.REFLECTING) {
783
+ if (this.isRunning()) {
476
784
  this.config.addTraceEntry('SELF_REFLECTION_SKIPPED', 'Self-reflection already in progress.');
477
785
  return;
478
786
  }
479
- const previousState = this.config.getState();
480
- this.config.setState(GMIPrimeState.REFLECTING);
787
+ await this.enqueue('Self-reflection', (run) => this.runSelfReflectionCycle(reflectionMetaPromptDef, run));
788
+ }
789
+ /**
790
+ * Runs one self-reflection cycle for the `gmi_self_trait_adjustment`
791
+ * metaprompt: gathers evidence, calls the LLM, and applies the parsed
792
+ * updates. Errors are recorded in the reasoning trace and not rethrown.
793
+ *
794
+ * @param reflectionMetaPromptDef - The `gmi_self_trait_adjustment` definition.
795
+ */
796
+ async runSelfReflectionCycle(reflectionMetaPromptDef, run) {
481
797
  this.config.addTraceEntry('SELF_REFLECTION_START', 'Starting self-reflection cycle.');
482
798
  try {
483
799
  const evidence = {
@@ -505,16 +821,27 @@ export class MetapromptExecutor {
505
821
  if (!provider) {
506
822
  throw new GMIError(`Provider '${providerId}' not found for self-reflection.`, GMIErrorCode.LLM_PROVIDER_UNAVAILABLE);
507
823
  }
824
+ if (run?.isStale()) {
825
+ this.config.addTraceEntry('WARNING', "Discarded late results of metaprompt 'gmi_self_trait_adjustment'.");
826
+ return;
827
+ }
508
828
  const llmResponse = await provider.generateCompletion(modelId, [{ role: 'user', content: metaPromptText }], {
509
829
  maxTokens: reflectionMetaPromptDef.maxOutputTokens || 512,
510
830
  temperature: reflectionMetaPromptDef.temperature || 0.3,
511
831
  responseFormat: { type: 'json_object' },
832
+ requestTimeout: this.runTimeoutMs,
512
833
  });
513
834
  const responseContent = llmResponse.choices?.[0]?.message?.content;
514
835
  if (!responseContent || typeof responseContent !== 'string') {
515
836
  throw new GMIError('Self-reflection LLM call returned no valid content.', GMIErrorCode.LLM_PROVIDER_ERROR, { response: llmResponse });
516
837
  }
517
838
  this.config.addTraceEntry('SELF_REFLECTION_DETAIL', 'LLM response for reflection received.', { preview: responseContent.substring(0, 100) });
839
+ // A cycle that went stale while the provider answered is discarded
840
+ // anyway, so no JSON repair call is made on its behalf.
841
+ if (run?.isStale()) {
842
+ this.config.addTraceEntry('WARNING', "Discarded late results of metaprompt 'gmi_self_trait_adjustment'.");
843
+ return;
844
+ }
518
845
  const parseOptions = {
519
846
  attemptFixWithLLM: true,
520
847
  llmModelIdForFix: reflectionMetaPromptDef.modelId || modelId,
@@ -526,10 +853,11 @@ export class MetapromptExecutor {
526
853
  }
527
854
  this.config.addTraceEntry('SELF_REFLECTION_DETAIL', 'Parsed trait update suggestions.', { suggestions: parsedUpdates });
528
855
  // Apply updates via callbacks
529
- await this.applyMetapromptUpdates(parsedUpdates, 'gmi_self_trait_adjustment');
530
- // Handle memory imprints specifically for the trace
531
- if (parsedUpdates.newMemoryImprints && parsedUpdates.newMemoryImprints.length > 0) {
532
- this.config.addTraceEntry('STATE_CHANGE', 'New memory imprints added from self-reflection.', { imprints: parsedUpdates.newMemoryImprints.map((i) => i.key) });
856
+ const storedImprintKeys = await this.applyMetapromptUpdates(parsedUpdates, 'gmi_self_trait_adjustment', run);
857
+ // Trace only the imprints that were stored: a late or stale run
858
+ // stores none, and later reflections read this trace as evidence.
859
+ if (storedImprintKeys.length > 0) {
860
+ this.config.addTraceEntry('STATE_CHANGE', 'New memory imprints added from self-reflection.', { imprints: storedImprintKeys });
533
861
  }
534
862
  if (parsedUpdates.adjustmentRationale) {
535
863
  this.config.addTraceEntry('SELF_REFLECTION_DETAIL', 'Self-reflection rationale recorded.', { rationale: parsedUpdates.adjustmentRationale });
@@ -541,11 +869,6 @@ export class MetapromptExecutor {
541
869
  console.error(`GMI (ID: ${this.config.getGmiId()}) self-reflection error:`, gmiError);
542
870
  }
543
871
  finally {
544
- const disallowedStates = new Set([
545
- GMIPrimeState.IDLE,
546
- GMIPrimeState.INITIALIZING,
547
- ]);
548
- this.config.setState(disallowedStates.has(previousState) ? GMIPrimeState.READY : previousState);
549
872
  this.config.addTraceEntry('SELF_REFLECTION_COMPLETE', 'Self-reflection cycle finished.');
550
873
  }
551
874
  }