@animalabs/agent-framework 0.33.0 → 0.34.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 (176) hide show
  1. package/README.md +136 -8
  2. package/dist/src/agent.d.ts +25 -4
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +68 -13
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/append-all.d.ts +5 -0
  7. package/dist/src/append-all.d.ts.map +1 -0
  8. package/dist/src/append-all.js +8 -0
  9. package/dist/src/append-all.js.map +1 -0
  10. package/dist/src/context-options.d.ts +9 -6
  11. package/dist/src/context-options.d.ts.map +1 -1
  12. package/dist/src/context-options.js +13 -8
  13. package/dist/src/context-options.js.map +1 -1
  14. package/dist/src/cut-off-round.js +4 -4
  15. package/dist/src/dendrite/registry.d.ts.map +1 -1
  16. package/dist/src/dendrite/registry.js +4 -2
  17. package/dist/src/dendrite/registry.js.map +1 -1
  18. package/dist/src/failure-text.d.ts +36 -0
  19. package/dist/src/failure-text.d.ts.map +1 -0
  20. package/dist/src/failure-text.js +89 -0
  21. package/dist/src/failure-text.js.map +1 -0
  22. package/dist/src/framework.d.ts +94 -30
  23. package/dist/src/framework.d.ts.map +1 -1
  24. package/dist/src/framework.js +987 -284
  25. package/dist/src/framework.js.map +1 -1
  26. package/dist/src/gate/event-gate.d.ts +40 -6
  27. package/dist/src/gate/event-gate.d.ts.map +1 -1
  28. package/dist/src/gate/event-gate.js +66 -25
  29. package/dist/src/gate/event-gate.js.map +1 -1
  30. package/dist/src/gate/reply-judge.d.ts +4 -0
  31. package/dist/src/gate/reply-judge.d.ts.map +1 -1
  32. package/dist/src/gate/reply-judge.js +12 -3
  33. package/dist/src/gate/reply-judge.js.map +1 -1
  34. package/dist/src/gate/suppression-review.js +2 -2
  35. package/dist/src/gate/suppression-review.js.map +1 -1
  36. package/dist/src/gate/types.d.ts +7 -0
  37. package/dist/src/gate/types.d.ts.map +1 -1
  38. package/dist/src/heartbeat/heartbeat.js +1 -1
  39. package/dist/src/heartbeat/heartbeat.js.map +1 -1
  40. package/dist/src/index.d.ts +1 -0
  41. package/dist/src/index.d.ts.map +1 -1
  42. package/dist/src/index.js +1 -0
  43. package/dist/src/index.js.map +1 -1
  44. package/dist/src/inference-bounds.d.ts +2 -1
  45. package/dist/src/inference-bounds.d.ts.map +1 -1
  46. package/dist/src/inference-bounds.js +9 -6
  47. package/dist/src/inference-bounds.js.map +1 -1
  48. package/dist/src/mcpl/channel-registry.d.ts +57 -13
  49. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  50. package/dist/src/mcpl/channel-registry.js +136 -24
  51. package/dist/src/mcpl/channel-registry.js.map +1 -1
  52. package/dist/src/mcpl/modern-connection.d.ts +109 -15
  53. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  54. package/dist/src/mcpl/modern-connection.js +593 -86
  55. package/dist/src/mcpl/modern-connection.js.map +1 -1
  56. package/dist/src/mcpl/protocol-family.d.ts +3 -1
  57. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  58. package/dist/src/mcpl/protocol-family.js +15 -9
  59. package/dist/src/mcpl/protocol-family.js.map +1 -1
  60. package/dist/src/mcpl/server-connection.d.ts +37 -18
  61. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  62. package/dist/src/mcpl/server-connection.js +31 -16
  63. package/dist/src/mcpl/server-connection.js.map +1 -1
  64. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  65. package/dist/src/mcpl/tool-presence.js +10 -6
  66. package/dist/src/mcpl/tool-presence.js.map +1 -1
  67. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  68. package/dist/src/mcpl/toolset-host.js +7 -6
  69. package/dist/src/mcpl/toolset-host.js.map +1 -1
  70. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  71. package/dist/src/mcpl/toolsets-module.js +2 -1
  72. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  73. package/dist/src/mcpl/types.d.ts +25 -4
  74. package/dist/src/mcpl/types.d.ts.map +1 -1
  75. package/dist/src/mcpl/types.js.map +1 -1
  76. package/dist/src/modules/discord/index.js +1 -1
  77. package/dist/src/modules/discord/index.js.map +1 -1
  78. package/dist/src/modules/health/index.js +2 -2
  79. package/dist/src/modules/health/index.js.map +1 -1
  80. package/dist/src/modules/history/semantic.js +3 -3
  81. package/dist/src/modules/history/semantic.js.map +1 -1
  82. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  83. package/dist/src/modules/workspace/index.js +4 -3
  84. package/dist/src/modules/workspace/index.js.map +1 -1
  85. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  86. package/dist/src/modules/workspace/observe.js +2 -1
  87. package/dist/src/modules/workspace/observe.js.map +1 -1
  88. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  89. package/dist/src/modules/workspace/reconcile.js +3 -2
  90. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  91. package/dist/src/notice-tags.d.ts +10 -8
  92. package/dist/src/notice-tags.d.ts.map +1 -1
  93. package/dist/src/notice-tags.js +10 -8
  94. package/dist/src/notice-tags.js.map +1 -1
  95. package/dist/src/presentation.d.ts +2 -1
  96. package/dist/src/presentation.d.ts.map +1 -1
  97. package/dist/src/presentation.js +4 -2
  98. package/dist/src/presentation.js.map +1 -1
  99. package/dist/src/reasoning-loss.d.ts +5 -0
  100. package/dist/src/reasoning-loss.d.ts.map +1 -0
  101. package/dist/src/reasoning-loss.js +8 -0
  102. package/dist/src/reasoning-loss.js.map +1 -0
  103. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  104. package/dist/src/recovery/offline-branch.js +5 -4
  105. package/dist/src/recovery/offline-branch.js.map +1 -1
  106. package/dist/src/recovery/recover-cli.js +2 -1
  107. package/dist/src/recovery/recover-cli.js.map +1 -1
  108. package/dist/src/sent-rows.js +1 -1
  109. package/dist/src/supplied-text.d.ts +1 -1
  110. package/dist/src/supplied-text.js +3 -3
  111. package/dist/src/supplied-text.js.map +1 -1
  112. package/dist/src/thinking-drops.d.ts +2 -0
  113. package/dist/src/thinking-drops.d.ts.map +1 -1
  114. package/dist/src/thinking-drops.js.map +1 -1
  115. package/dist/src/tool-presentation.d.ts.map +1 -1
  116. package/dist/src/tool-presentation.js +38 -9
  117. package/dist/src/tool-presentation.js.map +1 -1
  118. package/dist/src/tune-out/coordinator.d.ts +13 -4
  119. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  120. package/dist/src/tune-out/coordinator.js +33 -16
  121. package/dist/src/tune-out/coordinator.js.map +1 -1
  122. package/dist/src/tune-out/tools.d.ts.map +1 -1
  123. package/dist/src/tune-out/tools.js +2 -1
  124. package/dist/src/tune-out/tools.js.map +1 -1
  125. package/dist/src/types/agent.d.ts +39 -5
  126. package/dist/src/types/agent.d.ts.map +1 -1
  127. package/dist/src/types/framework.d.ts +43 -8
  128. package/dist/src/types/framework.d.ts.map +1 -1
  129. package/dist/src/types/inference-log.d.ts +1 -0
  130. package/dist/src/types/inference-log.d.ts.map +1 -1
  131. package/dist/src/types/trace.d.ts +64 -4
  132. package/dist/src/types/trace.d.ts.map +1 -1
  133. package/package.json +3 -3
  134. package/src/agent.ts +71 -14
  135. package/src/append-all.ts +6 -0
  136. package/src/context-options.ts +18 -8
  137. package/src/cut-off-round.ts +4 -4
  138. package/src/dendrite/registry.ts +4 -2
  139. package/src/failure-text.ts +88 -0
  140. package/src/framework.ts +1018 -293
  141. package/src/gate/event-gate.ts +90 -26
  142. package/src/gate/reply-judge.ts +12 -3
  143. package/src/gate/suppression-review.ts +2 -2
  144. package/src/gate/types.ts +7 -0
  145. package/src/heartbeat/heartbeat.ts +1 -1
  146. package/src/index.ts +1 -0
  147. package/src/inference-bounds.ts +11 -6
  148. package/src/mcpl/channel-registry.ts +160 -31
  149. package/src/mcpl/modern-connection.ts +629 -91
  150. package/src/mcpl/protocol-family.ts +15 -9
  151. package/src/mcpl/server-connection.ts +28 -15
  152. package/src/mcpl/tool-presence.ts +10 -5
  153. package/src/mcpl/toolset-host.ts +7 -6
  154. package/src/mcpl/toolsets-module.ts +2 -1
  155. package/src/mcpl/types.ts +25 -4
  156. package/src/modules/discord/index.ts +1 -1
  157. package/src/modules/health/index.ts +2 -2
  158. package/src/modules/history/semantic.ts +3 -3
  159. package/src/modules/workspace/index.ts +4 -3
  160. package/src/modules/workspace/observe.ts +2 -1
  161. package/src/modules/workspace/reconcile.ts +3 -2
  162. package/src/notice-tags.ts +11 -8
  163. package/src/presentation.ts +4 -2
  164. package/src/reasoning-loss.ts +7 -0
  165. package/src/recovery/offline-branch.ts +5 -4
  166. package/src/recovery/recover-cli.ts +2 -1
  167. package/src/sent-rows.ts +1 -1
  168. package/src/supplied-text.ts +3 -3
  169. package/src/thinking-drops.ts +2 -0
  170. package/src/tool-presentation.ts +34 -9
  171. package/src/tune-out/coordinator.ts +32 -15
  172. package/src/tune-out/tools.ts +2 -1
  173. package/src/types/agent.ts +36 -5
  174. package/src/types/framework.ts +44 -8
  175. package/src/types/inference-log.ts +1 -0
  176. package/src/types/trace.ts +66 -4
@@ -1,3 +1,5 @@
1
+ import { isReasoningLoss } from './reasoning-loss.js';
2
+ export { residentViewFilter, subconsciousViewFilter } from './context-options.js';
1
3
  import { ToolPresentation, presentationTools, isPresentationTool, renderCatalogue } from "./tool-presentation.js";
2
4
  import { presentAuthor } from './author-presentation.js';
3
5
  import { constants as bufferConstants } from 'node:buffer';
@@ -9,7 +11,8 @@ import { JsStore } from '@animalabs/chronicle';
9
11
  import { MembraneError } from '@animalabs/membrane';
10
12
  import { classifyFailure, retryDecision, MEMBRANE_RETRY_SETTINGS } from '@animalabs/membrane/retry';
11
13
  import { deadlineCut, isDeadlineExpiry, resolveInferenceBounds, wholeCallMs } from './inference-bounds.js';
12
- import { ContextManager, PassthroughStrategy, OverBudgetError, UncoveredDropError, stripUnpairedToolBlocks, continuesBody, MalformedContentError, malformedContentPlaces, CompressionWithdrawn } from '@animalabs/context-manager';
14
+ import { boundFailureText, compileRefusalType, failureWords, FAILURE_TEXT_MAX_CHARS, houseReason, isMembraneError, PROVIDER_ERROR_CODE_MAX_CHARS } from './failure-text.js';
15
+ import { ContextManager, PassthroughStrategy, stripUnpairedToolBlocks, continuesBody, MalformedContentError, malformedContentPlaces, CompressionWithdrawn } from '@animalabs/context-manager';
13
16
  // Not named imports from context-manager: 0.24.0 has neither, and a named
14
17
  // import of either would fail this package's load there before create()
15
18
  // could name the release it needs.
@@ -80,6 +83,7 @@ import { AgentPresentation, asCompiled, bearingOf, heldFromResidents, suppressed
80
83
  import { coverTest, covers, readMarkThrough } from './read-marks.js';
81
84
  import { contentAsStored, nameAsStored } from './text-as-stored.js';
82
85
  import { STORED_INJECTION_KEY, STORED_INJECTION_KIND, asStoredRow, hasRenderedContent, isOwnInjection, isStoredKind, storedInjectionMarkOf, unitHash } from './stored-injections.js';
86
+ import { appendAll } from './append-all.js';
83
87
  /** An MCPL tool call refused because its server isn't in the registry: one
84
88
  * closed for good, one removed, or one whose connect failed without
85
89
  * reconnect, which this host doesn't bring back on its own. A server that's
@@ -916,10 +920,15 @@ const TURN_RETRY = Symbol('turn-retry');
916
920
  */
917
921
  const MERGED_BATCH = Symbol('merged-batch');
918
922
  const REARMED = Symbol('rearmed');
923
+ const UNPROMPTED = Symbol('unprompted');
919
924
  const TURN_CARRY = Symbol('turn-carry');
925
+ const BUDGET_WAIT = Symbol('budget-wait');
920
926
  function isRearmed(request) {
921
927
  return request !== undefined && request[REARMED] === true;
922
928
  }
929
+ function isUnprompted(request) {
930
+ return request !== undefined && request[UNPROMPTED] === true;
931
+ }
923
932
  function waitAlertsOnItsOwn(wait) {
924
933
  return wait.until === null || wait.until - wait.setAt > PROVIDER_WAIT_ALERT_MS;
925
934
  }
@@ -965,6 +974,9 @@ function asTurnRetry(trigger) {
965
974
  function continuesTurn(request) {
966
975
  return resumesLogicalTurn(request.reason) || isTurnRetry(request);
967
976
  }
977
+ function budgetWaitOf(request) {
978
+ return request ? request[BUDGET_WAIT] : undefined;
979
+ }
968
980
  function turnCarryOf(request) {
969
981
  return request && continuesTurn(request) ? request[TURN_CARRY] : undefined;
970
982
  }
@@ -1143,6 +1155,7 @@ const RETRY_CANCELLED_BY_STOP = 'a retry waiting while the framework was stoppin
1143
1155
  /** The reason on the inference:aborted that ends a turn whose continuation
1144
1156
  * was dropped before it started, other than by an abandon or a stop. */
1145
1157
  const CONTINUATION_DROPPED = "a turn's continuation was dropped before it started";
1158
+ const BUDGET_HELD_TURN_REMOVED = 'a turn that waited for compression was dropped: what it stood on was removed';
1146
1159
  function isTurnContinuation(reason) {
1147
1160
  return reason === 'context_budget_restart' || reason === 'tool_results_ready'
1148
1161
  || reason === 'tool_result_guard_retry';
@@ -1165,6 +1178,9 @@ const FRAMEWORK_WAKE_SOURCES = new Set(['framework', 'scheduler']);
1165
1178
  function hasOwnCauseForRun(r) {
1166
1179
  return !FRAMEWORK_WAKE_SOURCES.has(r.source) && r.suppressProse !== true;
1167
1180
  }
1181
+ function bringsOwnChannels(r) {
1182
+ return hasOwnCauseForRun(r) && (r.wakeChannels?.length ?? 0) > 0;
1183
+ }
1168
1184
  /** Whether a wake carries an autoContinue continuation: its own wake, or a
1169
1185
  * requeued turn trigger that kept the mark (InferenceRequest.autoContinueRun). */
1170
1186
  function carriesContinuation(r) {
@@ -1264,6 +1280,10 @@ function assertFrameworkSwitches(config) {
1264
1280
  assertBooleanSetting('watchdog.enabled', config.watchdog.enabled, 'true arms the liveness watchdog, which ends the process when the main thread misses its heartbeat for thresholdMs, ' +
1265
1281
  'and false or leaving it out doesn\'t');
1266
1282
  }
1283
+ const unprompted = config.unpromptedChannel;
1284
+ if (unprompted !== undefined && unprompted !== null && (typeof unprompted !== 'string' || unprompted.trim() === '')) {
1285
+ throw new Error(`unpromptedChannel must name a channel, such as "discord:<guildId>:<channelId>", not ${typeof unprompted === 'string' ? 'a blank string' : kindOf(unprompted)}`);
1286
+ }
1267
1287
  // The heartbeat: true or an object turns it on; false, null or leaving it
1268
1288
  // out leaves it off. Any other value (a "false" read from text, a 0) is
1269
1289
  // refused rather than read as on.
@@ -1627,8 +1647,8 @@ export class QuiesceBlockedError extends Error {
1627
1647
  */
1628
1648
  export class ProviderWaitNotSleptError extends Error {
1629
1649
  retryAfterMs;
1630
- constructor(message, retryAfterMs) {
1631
- super(message);
1650
+ constructor(message, retryAfterMs, options) {
1651
+ super(message, options);
1632
1652
  this.retryAfterMs = retryAfterMs;
1633
1653
  this.name = 'ProviderWaitNotSleptError';
1634
1654
  }
@@ -1779,7 +1799,7 @@ export class DefaultErrorPolicy {
1779
1799
  settings;
1780
1800
  constructor(settings = TURN_RETRY_SETTINGS) {
1781
1801
  this.settings = settings;
1782
- this.maxRetries = Math.max(0, ...Object.values(settings).map((s) => (s?.maxAttempts ?? 1) - 1));
1802
+ this.maxRetries = Object.values(settings).reduce((max, s) => Math.max(max, (s?.maxAttempts ?? 1) - 1), 0);
1783
1803
  }
1784
1804
  onInferenceError(error, _agentName, attempt) {
1785
1805
  if (isDeadlineExpiry(error)) {
@@ -1885,6 +1905,10 @@ const TURN_RETRY_SETTINGS = {
1885
1905
  deadline: { maxAttempts: DEADLINE_TURN_TRIES, firstDelayMs: 1_000, multiplier: 1, maxDelayMs: 1_000, statedWaitCeilingMs: null },
1886
1906
  };
1887
1907
  const PROVIDER_ACCELERATION_JITTER_MS = 5_000;
1908
+ const BUDGET_HOLD_NOTE_NAMESPACE = 'framework:budget-hold';
1909
+ function describeWait(ms) {
1910
+ return ms < 60_000 ? `${Math.max(1, Math.round(ms / 1000))} s` : `${Math.round(ms / 60_000)} min`;
1911
+ }
1888
1912
  /** Longest delay one setTimeout can represent (2^31-1 ms, about 24.8 days);
1889
1913
  * a longer timer fires almost at once, so longer waits re-arm in steps. */
1890
1914
  const MAX_TIMER_DELAY_MS = 2_147_483_647;
@@ -1900,16 +1924,6 @@ const WAITING_TRACE_EVERY_MS = 60_000;
1900
1924
  * waits in a row hold one turn longer than this (noteStatedWaitRun,
1901
1925
  * endStatedWaitRun). */
1902
1926
  const PROVIDER_WAIT_ALERT_MS = 60 * 60_000;
1903
- /**
1904
- * A MembraneError, from this framework's copy of membrane or from another: the
1905
- * host supplies the Membrane instance (config.membrane), so with two copies
1906
- * installed its errors are instances of a class this framework's `instanceof`
1907
- * doesn't match. Recognised by name as well, as context-manager's errors are;
1908
- * its fields are the same, so callers still check each one's type.
1909
- */
1910
- function isMembraneError(error) {
1911
- return error instanceof MembraneError || (error instanceof Error && error.name === 'MembraneError');
1912
- }
1913
1927
  /**
1914
1928
  * agent-framework#734. Membrane's own streams send a retryable failure as an
1915
1929
  * `error` event and send nothing after their terminal event. A stream of
@@ -2171,42 +2185,16 @@ const SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES = Buffer.byteLength(JSON.stringify({ con
2171
2185
  * failure naming the tool and saying the call itself completed.
2172
2186
  */
2173
2187
  const scriptResultMaxChars = (memoryMaxBytes) => Math.max(0, Math.floor((Math.min(maxResultBytes(memoryMaxBytes), bufferConstants.MAX_STRING_LENGTH) - SCRIPT_TEXT_ANSWER_ENVELOPE_BYTES) / 6));
2174
- /**
2175
- * Longest failure text the inference-failure pipeline writes to one sink: the
2176
- * reason in the exhaustion stderr line, a field of a logs/failures.log record,
2177
- * an ops alert's message, the inference log's `error`, and a trace event's
2178
- * `error` and `stack`. A failure's text is whatever its source put in
2179
- * `Error.message`, and a provider can echo the entire rejected request there
2180
- * (one OpenAI-compatible 400 echoed ~1.7 MB). The sinks with shorter bounds
2181
- * name them below, and every cut goes through boundFailureText. This bound
2182
- * doesn't depend on membrane bounding its own errors.
2183
- */
2184
- const FAILURE_TEXT_MAX_CHARS = 2_000;
2185
- /** Longest failure reason written into an agent's own context (the
2186
- * `[inference-failed]` marker): enough for a provider's explanation, not a
2187
- * self-dump the agent then carries in every later request. */
2188
+ /** Longest failure reason in the `[inference-failed]` marker: the reason its
2189
+ * text gives, and the excerpt its metadata keeps. */
2188
2190
  const FAILURE_MARKER_REASON_MAX_CHARS = 600;
2189
- /** Longest failure reason in the rewind marker, also the agent's own context:
2190
- * enough to identify the failure class without a wall of provider JSON. */
2191
+ /** Longest failure reason the rewind marker's metadata keeps as its category. */
2191
2192
  const REWIND_MARKER_REASON_MAX_CHARS = 160;
2192
2193
  /** Longest failure reason in an agent's status (`lastInference.lastError`) and
2193
2194
  * in the compile-refused ops alert. */
2194
2195
  const FAILURE_SUMMARY_MAX_CHARS = 300;
2195
2196
  /** Longest alert message posted to the ops webhook. */
2196
2197
  const OPS_WEBHOOK_MESSAGE_MAX_CHARS = 500;
2197
- /** Longest provider error code recorded verbatim; real codes are short tokens.
2198
- * A failure's name and type are bounded the same way. */
2199
- const PROVIDER_ERROR_CODE_MAX_CHARS = 128;
2200
- const isHighSurrogate = (text, index) => {
2201
- const code = text.charCodeAt(index);
2202
- return code >= 0xd800 && code <= 0xdbff;
2203
- };
2204
- const isLowSurrogate = (text, index) => {
2205
- const code = text.charCodeAt(index);
2206
- return code >= 0xdc00 && code <= 0xdfff;
2207
- };
2208
- /** Whether a cut before `index` would separate the halves of a surrogate pair. */
2209
- const splitsPair = (text, index) => index > 0 && index < text.length && isHighSurrogate(text, index - 1) && isLowSurrogate(text, index);
2210
2198
  /**
2211
2199
  * Whether an MCPL server's child ended the way a server ends when it's asked
2212
2200
  * to: exit 0, death by a signal that asks a process to end (SIGTERM, SIGINT,
@@ -2231,28 +2219,6 @@ function closeWords(reason, code, signal) {
2231
2219
  const how = [reason ?? null, code != null ? `code ${code}` : null, signal ? `signal ${signal}` : null].filter((part) => part !== null);
2232
2220
  return how.length > 0 ? ` (${how.join(', ')})` : '';
2233
2221
  }
2234
- /**
2235
- * `text` when it fits within `max` characters; otherwise its head and tail
2236
- * around a marker stating how many of how many characters were omitted, the
2237
- * whole within `max`. A cut that would separate a surrogate pair moves toward
2238
- * omission, so the result never holds half of a pair the text held whole, and
2239
- * bounding bounded text returns it as is. Each sink cuts the whole text, so
2240
- * its marker states the whole length.
2241
- */
2242
- function boundFailureText(text, max = FAILURE_TEXT_MAX_CHARS) {
2243
- if (text.length <= max)
2244
- return text;
2245
- const markerLength = ` …[${text.length} of ${text.length} characters omitted]… `.length;
2246
- const budget = Math.max(0, max - markerLength);
2247
- const tailLength = Math.floor(budget / 4);
2248
- let headEnd = budget - tailLength;
2249
- if (splitsPair(text, headEnd))
2250
- headEnd--;
2251
- let tailStart = text.length - tailLength;
2252
- if (splitsPair(text, tailStart))
2253
- tailStart++;
2254
- return `${text.slice(0, headEnd)} …[${tailStart - headEnd} of ${text.length} characters omitted]… ${text.slice(tailStart)}`;
2255
- }
2256
2222
  export function opsWebhookText(message) {
2257
2223
  return boundFailureText(message, OPS_WEBHOOK_MESSAGE_MAX_CHARS);
2258
2224
  }
@@ -2326,6 +2292,30 @@ const UNDESCRIBED_FAILURE = `[${NOTICE_TAG.undescribedFailure}]`;
2326
2292
  function failureShown(text) {
2327
2293
  return text === UNDESCRIBED_FAILURE ? text : suppliedValue(text);
2328
2294
  }
2295
+ function failedEntry(prefix, err) {
2296
+ return { error: `${prefix}${err.message}`, errorWords: `${prefix}${failureWords(err)}` };
2297
+ }
2298
+ function shownReasonOf(err) {
2299
+ const own = houseReason(err);
2300
+ return own === undefined ? {} : { shownReason: own };
2301
+ }
2302
+ function boundedCause(err) {
2303
+ const cause = new Error(boundFailureText(String(err.message)));
2304
+ cause.name = boundFailureText(String(err.name), PROVIDER_ERROR_CODE_MAX_CHARS);
2305
+ cause.stack = boundFailureText(String(err.stack ?? `${cause.name}: ${cause.message}`));
2306
+ if (isMembraneError(err)) {
2307
+ const fields = err;
2308
+ const code = boundProviderErrorCode(fields.providerErrorCode);
2309
+ Object.assign(cause, {
2310
+ ...(typeof fields.type === 'string' ? { type: boundFailureText(fields.type, PROVIDER_ERROR_CODE_MAX_CHARS) } : {}),
2311
+ ...(typeof fields.retryable === 'boolean' ? { retryable: fields.retryable } : {}),
2312
+ ...(typeof fields.httpStatus === 'number' ? { httpStatus: fields.httpStatus } : {}),
2313
+ ...(code !== undefined ? { providerErrorCode: code } : {}),
2314
+ ...(typeof fields.retryAfterMs === 'number' ? { retryAfterMs: fields.retryAfterMs } : {}),
2315
+ });
2316
+ }
2317
+ return cause;
2318
+ }
2329
2319
  /**
2330
2320
  * A bounded string describing a failure, for logs that would otherwise print
2331
2321
  * the error object; it is one line unless one of its parts has newlines, as a
@@ -2648,6 +2638,14 @@ function mergeWakeLines(requests) {
2648
2638
  }
2649
2639
  return byEntry.size > 0 ? [...byEntry.values()] : undefined;
2650
2640
  }
2641
+ function wakeChannelsOf(requests) {
2642
+ const channels = [];
2643
+ for (const channel of requests.flatMap((r) => r?.wakeChannels ?? [])) {
2644
+ if (!channels.some((c) => sameChannel(c, channel)))
2645
+ channels.push(channel);
2646
+ }
2647
+ return channels.length > 0 ? channels : undefined;
2648
+ }
2651
2649
  /** A batched-wake line's identity: its event's stored entry. */
2652
2650
  function wakeLineKey(line) {
2653
2651
  const { eventType, serverId, channelId, messageId, eventId } = line.entry;
@@ -2805,6 +2803,9 @@ export class AgentFramework {
2805
2803
  providerAccelerationCooldowns = new Map();
2806
2804
  providerAccelerationRecoveries = new Map();
2807
2805
  providerAccelerationLastRecovery = new Map();
2806
+ budgetHolds = new Map();
2807
+ overBudgetDrainAfter = new Map();
2808
+ budgetHoldRecheckMs = DEFAULT_MAINTENANCE_INTERVAL_MS;
2808
2809
  providerAccelerationDefaultCooldownMs = PROVIDER_ACCELERATION_DEFAULT_COOLDOWN_MS;
2809
2810
  providerAccelerationJitterMs = PROVIDER_ACCELERATION_JITTER_MS;
2810
2811
  inferenceBounds = resolveInferenceBounds(undefined);
@@ -3305,11 +3306,11 @@ export class AgentFramework {
3305
3306
  managerReleases = new WeakMap();
3306
3307
  releasedByCleanup = new WeakSet();
3307
3308
  resumesPending = new Map();
3308
- // context-manager 0.23.1: a strategy that keeps its context, as
3309
- // AutobiographicalStrategy does (hostContext, autobiographical.ts:2897),
3309
+ // context-manager 0.25.1: a strategy that keeps its context, as
3310
+ // AutobiographicalStrategy does (hostContext, autobiographical.ts:3156),
3310
3311
  // keeps its manager reachable while its work is pending, since the
3311
3312
  // context's tools and systemPrompt getters close over the manager
3312
- // (context-manager.ts:2343-2359). So such a released manager is collected
3313
+ // (context-manager.ts:4602-4621). So such a released manager is collected
3313
3314
  // only once nothing of its work can still read the store, and stop()
3314
3315
  // closes, before an owned store, each one alive.
3315
3316
  openManagers = new Set();
@@ -3433,7 +3434,8 @@ export class AgentFramework {
3433
3434
  toolPatternKnownPrefixes = new Map();
3434
3435
  /** Maps serverId → McplServerConfig for prefix lookup. */
3435
3436
  mcplServerConfigs = new Map();
3436
- /** Modern-MCP (2026-07-28) servers, by id; read through modernServers().
3437
+ /** Modern-MCP servers (2026-07-28, or a 2025 revision over HTTP), by id;
3438
+ * read through modernServers().
3437
3439
  * They are kept apart from the MCPL registry on purpose: they have no
3438
3440
  * grant, planes or server→host traffic, so the MCPL machinery (quiesce's
3439
3441
  * plane pauses, policy) never sees them. They take part
@@ -3470,6 +3472,7 @@ export class AgentFramework {
3470
3472
  * subsystem can be lazily initialized by connectMcplServer when the
3471
3473
  * framework started with zero configured servers. */
3472
3474
  mcplInferenceRoutingConfig = null;
3475
+ mcplUnpromptedChannel = null;
3473
3476
  /** The awareness ledger; null for a framework with neither storePath nor
3474
3477
  * discordAwarenessOutboxPath, which refuses a marks choice. */
3475
3478
  discordAwarenessOutbox;
@@ -3580,12 +3583,15 @@ export class AgentFramework {
3580
3583
  assertAgentNamesUnique(config.agents);
3581
3584
  assertObservedAgentsConfigured(config.agents);
3582
3585
  // The presentation index (#291) needs a context-manager that keeps a
3583
- // window index per agent (presentRows): 0.25.0 on, since 0.23.x and
3584
- // 0.24.0 were published without one. It needs no store, so it is
3585
- // checked before one is opened.
3586
- if (typeof ContextManager.prototype.presentRows !== 'function') {
3587
- throw new Error('agent-framework needs @animalabs/context-manager with ContextManager.presentRows: a context-manager that keeps a window index per agent. ' +
3588
- 'Install @animalabs/context-manager ^0.25.0, which this package declares (0.23.x and 0.24.0 have no window index)');
3586
+ // window index per agent (presentRows), which 0.23.x and 0.24.x were
3587
+ // published without, and a stream tells it of dropped thinking
3588
+ // (noteThinkingDropped), which 0.25.0 was published without. It needs no
3589
+ // store, so it is checked before one is opened.
3590
+ const contextManagerProto = ContextManager.prototype;
3591
+ if (typeof contextManagerProto.presentRows !== 'function' || typeof contextManagerProto.noteThinkingDropped !== 'function') {
3592
+ throw new Error('agent-framework needs @animalabs/context-manager with ContextManager.presentRows and ContextManager.noteThinkingDropped: ' +
3593
+ "a context-manager that keeps a window index per agent and can be told which stored replies' thinking the provider dropped. " +
3594
+ 'Install @animalabs/context-manager ^0.25.1, which this package declares (0.23.x and 0.24.x have no window index, and 0.25.0 has no noteThinkingDropped)');
3589
3595
  }
3590
3596
  // Create or use existing store
3591
3597
  let store;
@@ -3982,7 +3988,7 @@ export class AgentFramework {
3982
3988
  : undefined);
3983
3989
  },
3984
3990
  requestInference: (agentName, reason, source, provenance, after) => {
3985
- framework.pendingRequests.push({
3991
+ const request = {
3986
3992
  agentName, reason, source, timestamp: Date.now(),
3987
3993
  ...framework.readsRowFor(after, agentName),
3988
3994
  // gate-requested wakes carry where/who (EventGate wakeProvenance)
@@ -4010,11 +4016,19 @@ export class AgentFramework {
4010
4016
  const route = provenance?.routeChannelId !== undefined
4011
4017
  ? framework.channelNamed(provenance.routeChannelId, provenance.routeServerId)
4012
4018
  : undefined;
4013
- return route && framework.mayTakeGateRoute(agentName, route)
4014
- ? { channelId: route.channelId, channelServerId: route.serverId, addressed: true }
4015
- : {};
4019
+ if (route && framework.mayTakeGateRoute(agentName, route)) {
4020
+ return { channelId: route.channelId, channelServerId: route.serverId, addressed: true };
4021
+ }
4022
+ const came = (provenance?.batchChannels ?? [])
4023
+ .map((c) => framework.channelNamed(c.channelId, c.serverId))
4024
+ .filter((c) => c !== undefined)
4025
+ .filter((c, i, all) => all.findIndex((d) => sameChannel(d, c)) === i);
4026
+ return came.length > 0 ? { wakeChannels: came } : {};
4016
4027
  })(),
4017
- });
4028
+ };
4029
+ if (source === 'self-wake' || source === 'sleep')
4030
+ request[UNPROMPTED] = true;
4031
+ framework.pendingRequests.push(request);
4018
4032
  },
4019
4033
  getAgentNames: () => framework.registry.gateRecipients(),
4020
4034
  isHeld: (serverId, channelId) => !!(serverId && channelId && framework.tuneOutCoordinator?.holdFor(serverId, channelId)),
@@ -4119,6 +4133,7 @@ export class AgentFramework {
4119
4133
  // Stored for lazy MCPL initialization (connectMcplServer on a framework
4120
4134
  // that started with zero configured servers).
4121
4135
  framework.mcplInferenceRoutingConfig = config.inferenceRouting ?? null;
4136
+ framework.mcplUnpromptedChannel = config.unpromptedChannel ?? null;
4122
4137
  // Client-side programmatic tool calling (code_execution). Made only when
4123
4138
  // enabled — everything downstream gates on the field. create() checked
4124
4139
  // the configuration (validateCodeExecutionConfig).
@@ -4679,6 +4694,9 @@ export class AgentFramework {
4679
4694
  });
4680
4695
  // A stopped host must never hang behind its own cooldown.
4681
4696
  step(() => this.cancelProviderAdmission());
4697
+ for (const name of [...(this.budgetHolds?.keys() ?? [])]) {
4698
+ step(() => this.dropBudgetHold(name, { kind: 'shutdown', why: 'the framework is stopping' }));
4699
+ }
4682
4700
  // An open logical turn doesn't survive a stop.
4683
4701
  this.openLogical.clear();
4684
4702
  // Cancel the stream of every agent whose state holds one: streaming,
@@ -5063,8 +5081,11 @@ export class AgentFramework {
5063
5081
  // A wait recorded while this call was parked binds it too.
5064
5082
  this.refuseHeldProviderModel(agentName, model);
5065
5083
  gate.auxiliaryInFlight++;
5084
+ let landed = false;
5066
5085
  try {
5067
- return await run();
5086
+ const result = await run();
5087
+ landed = true;
5088
+ return result;
5068
5089
  }
5069
5090
  catch (error) {
5070
5091
  // Compression can be the first call to find a spent quota (idle agent,
@@ -5084,6 +5105,8 @@ export class AgentFramework {
5084
5105
  if (gate.auxiliaryInFlight === 0)
5085
5106
  for (const resolve of gate.idleWaiters.splice(0))
5086
5107
  resolve();
5108
+ if (landed)
5109
+ this.managerCallLanded(agentName);
5087
5110
  }
5088
5111
  }
5089
5112
  /**
@@ -5131,7 +5154,7 @@ export class AgentFramework {
5131
5154
  ? `a turn whose retries ran out, sent again (${cooldown.rearm.number} of ${cooldown.rearm.of})`
5132
5155
  : 'a turn held by a provider cooldown, when it ends' });
5133
5156
  }
5134
- wakes.push(...this.heartbeatWakesFor(agentName));
5157
+ appendAll(wakes, this.heartbeatWakesFor(agentName));
5135
5158
  return wakes;
5136
5159
  }
5137
5160
  /**
@@ -5318,6 +5341,16 @@ export class AgentFramework {
5318
5341
  auxiliaryMembraneFor(agentName) {
5319
5342
  return this.membraneAdmitting((run, model) => this.withAuxiliaryAdmission(agentName, run, model));
5320
5343
  }
5344
+ managerCallLanded(agentName) {
5345
+ const hold = this.budgetHolds?.get(agentName);
5346
+ if (!hold)
5347
+ return;
5348
+ hold.managerCalls++;
5349
+ if (hold.checkQueued)
5350
+ return;
5351
+ const look = setTimeout(() => this.releaseBudgetHoldOnEvidence(agentName), 0);
5352
+ look.unref?.();
5353
+ }
5321
5354
  /**
5322
5355
  * The reply judge's calls. None waits: under the agent's holds a call is
5323
5356
  * refused without being made, and the agent's turn stops the judge. A call
@@ -5374,14 +5407,17 @@ export class AgentFramework {
5374
5407
  this.withdrawMaintenanceUnderSurgery();
5375
5408
  this.withdrawMaintenanceWhenStopping(agentName);
5376
5409
  this.refuseHeldProviderModel(agentName, model);
5410
+ let result;
5377
5411
  try {
5378
- return await run();
5412
+ result = await run();
5379
5413
  }
5380
5414
  catch (error) {
5381
5415
  if (model !== undefined && error instanceof Error)
5382
5416
  this.recordProviderWait(agentName, model, error);
5383
5417
  throw error;
5384
5418
  }
5419
+ this.managerCallLanded(agentName);
5420
+ return result;
5385
5421
  });
5386
5422
  }
5387
5423
  ephemeralAuxiliaryMembraneFor(agentName, release) {
@@ -5512,13 +5548,28 @@ export class AgentFramework {
5512
5548
  held.push(request);
5513
5549
  return;
5514
5550
  }
5551
+ // Read the kept request's carry before marking it.
5552
+ const keptCarry = turnCarryOf(same);
5515
5553
  if (isTurnRetry(request))
5516
5554
  same[TURN_RETRY] = true;
5555
+ const wait = budgetWaitOf(request);
5556
+ if (wait && !budgetWaitOf(same))
5557
+ same[BUDGET_WAIT] = wait;
5517
5558
  // A merged start's batch (MERGED_BATCH) stays recorded on the request
5518
5559
  // kept in its place, which then stands for it, as the start did.
5519
5560
  const batch = mergedBatch(request);
5520
5561
  if (batch)
5521
5562
  same[MERGED_BATCH] = [...new Set([...(mergedBatch(same) ?? []), ...batch])];
5563
+ const carry = keptCarry ?? turnCarryOf(request);
5564
+ if (carry)
5565
+ same[TURN_CARRY] = carry;
5566
+ else
5567
+ delete same[TURN_CARRY];
5568
+ if (!isRearmed(request))
5569
+ delete same[REARMED];
5570
+ const channels = wakeChannelsOf([same, request]);
5571
+ if (channels)
5572
+ same.wakeChannels = channels;
5522
5573
  }
5523
5574
  consultProviderHold(error, agentName) {
5524
5575
  if (!this.providerHoldHook)
@@ -5577,7 +5628,7 @@ export class AgentFramework {
5577
5628
  // failure streak), and the wait applies to the calls that follow it.
5578
5629
  if (!acceleration && !hostHold) {
5579
5630
  return wait !== undefined && isMembraneError(error) && error.retryable === true
5580
- && this.holdForProviderWait(agent, wait, trigger ? [asTurnRetry(trigger)] : [], true);
5631
+ && this.holdForProviderWait(agent, wait, trigger ? [this.turnRetryOf(trigger, this.turnRouting.get(agent.name))] : [], true);
5581
5632
  }
5582
5633
  // As in holdForProviderWait: once stop() has closed admission, nothing is
5583
5634
  // held, and nothing started.
@@ -5591,7 +5642,7 @@ export class AgentFramework {
5591
5642
  // The failed turn's own request, kept as its retry (TURN_RETRY). An
5592
5643
  // auxiliary call's failure passes no trigger: it holds no turn of its own.
5593
5644
  if (trigger)
5594
- this.holdRequest(held, asTurnRetry(trigger));
5645
+ this.holdRequest(held, this.turnRetryOf(trigger, this.turnRouting.get(agent.name)));
5595
5646
  if (existing)
5596
5647
  clearTimeout(existing.timer);
5597
5648
  const timer = setTimeout(() => this.releaseProviderAccelerationCooldown(agent.name), Math.min(delayMs, MAX_TIMER_DELAY_MS));
@@ -5657,6 +5708,8 @@ export class AgentFramework {
5657
5708
  const request = { ...trigger, [REARMED]: true };
5658
5709
  delete request[TURN_RETRY];
5659
5710
  delete request[MERGED_BATCH];
5711
+ delete request.wakeChannels;
5712
+ Object.assign(request, this.wakeChannelsMark(routing));
5660
5713
  if (continuation) {
5661
5714
  request.reason = REARM_REASON;
5662
5715
  delete request.counterparty;
@@ -5711,7 +5764,7 @@ export class AgentFramework {
5711
5764
  return undefined;
5712
5765
  if (this.registry.homeChannel(agent.name) !== undefined) {
5713
5766
  const wait = this.recordProviderWait(agent.name, agent.model, error);
5714
- return error.retryable === true && wait !== undefined && this.holdForProviderWait(agent, wait, trigger ? [asTurnRetry(trigger)] : [], true)
5767
+ return error.retryable === true && wait !== undefined && this.holdForProviderWait(agent, wait, trigger ? [this.turnRetryOf(trigger, this.turnRouting.get(agent.name))] : [], true)
5715
5768
  ? 'held'
5716
5769
  : undefined;
5717
5770
  }
@@ -5731,18 +5784,490 @@ export class AgentFramework {
5731
5784
  const cap = `${PROVIDER_ACCELERATION_MAX_COOLDOWN_MS / 1000} s`;
5732
5785
  if (Number.isNaN(stated)) {
5733
5786
  return {
5734
- error: `${error.message} (the provider's stated wait is not a number, and an ephemeral run sleeps only a ` +
5787
+ error: `${failureWords(error)} (the provider's stated wait is not a number, and an ephemeral run sleeps only a ` +
5735
5788
  `stated wait of ${cap} or less, so it ends now)`,
5736
5789
  statedWaitMs: Number.NaN,
5737
5790
  };
5738
5791
  }
5739
5792
  const asked = Number.isFinite(stated) ? `${Math.ceil(stated / 1000)} s` : 'longer than can be represented';
5740
5793
  return {
5741
- error: `${error.message} (the provider asked to wait ${asked}; an ephemeral run doesn't sleep a stated wait ` +
5794
+ error: `${failureWords(error)} (the provider asked to wait ${asked}; an ephemeral run doesn't sleep a stated wait ` +
5742
5795
  `longer than ${cap}, so it ends now)`,
5743
5796
  statedWaitMs: stated,
5744
5797
  };
5745
5798
  }
5799
+ holdForBudgetRefusal(agent, error, retry, checkpoint, routing, baseline, closeWindow) {
5800
+ const { errorType } = this.classifyInferenceError(error);
5801
+ if (errorType !== 'over_budget' && errorType !== 'context_refusal')
5802
+ return false;
5803
+ if (!this.budgetHolds || !this.running || this.providerAdmissionClosed)
5804
+ return false;
5805
+ if (this.ephemeralRuns.has(agent.name) || this.agents.get(agent.name) !== agent || this.budgetHolds.has(agent.name))
5806
+ return false;
5807
+ if (!this.compressionPending(agent))
5808
+ return false;
5809
+ if (checkpoint !== undefined) {
5810
+ try {
5811
+ this.dropTurnCheckpoint(agent.name, checkpoint);
5812
+ }
5813
+ catch (error) {
5814
+ console.error(`[budget-hold] agent=${agent.name}: its refused start's checkpoint could not be dropped, so the refusal takes the error policy's path:`, error);
5815
+ return false;
5816
+ }
5817
+ }
5818
+ const turn = retry ?? { agentName: agent.name, reason: 'budget-hold-release', source: 'framework', timestamp: Date.now() };
5819
+ const carried = budgetWaitOf(turn);
5820
+ const startedAt = Date.now();
5821
+ const standsOn = !continuesTurn(turn) && carried
5822
+ ? (carried.row !== undefined ? [carried.row] : [])
5823
+ : this.rowsTurnStandsOn(agent, turn, routing);
5824
+ const wait = {
5825
+ since: carried?.since ?? startedAt,
5826
+ ...(!continuesTurn(turn) && standsOn[0] !== undefined ? { row: standsOn[0] } : {}),
5827
+ ...(carried?.alerted ? { alerted: true } : {}),
5828
+ };
5829
+ turn[BUDGET_WAIT] = wait;
5830
+ const figures = error;
5831
+ const hold = {
5832
+ startedAt,
5833
+ wait,
5834
+ turn,
5835
+ standsOn,
5836
+ wakes: [],
5837
+ errorType,
5838
+ reason: boundFailureText(String(error.message)),
5839
+ budget: typeof figures.budget === 'number' ? figures.budget : undefined,
5840
+ actual: typeof figures.actual === 'number' ? figures.actual : undefined,
5841
+ managerCalls: 0,
5842
+ progressAtHold: baseline ? baseline.progress : this.compressionProgressOf(agent),
5843
+ settingsAtHold: baseline ? baseline.settings : agent.contextSettingsChanges(),
5844
+ };
5845
+ this.budgetHolds.set(agent.name, hold);
5846
+ this.startBudgetHoldRecheck(agent, hold);
5847
+ closeWindow();
5848
+ const endGate = () => this.eventGate?.onInferenceEnded(agent.name);
5849
+ this.emitTrace({
5850
+ type: 'inference:held',
5851
+ agentName: agent.name,
5852
+ errorType,
5853
+ error: hold.reason,
5854
+ ...(hold.budget !== undefined ? { budget: hold.budget } : {}),
5855
+ ...(hold.actual !== undefined ? { actual: hold.actual } : {}),
5856
+ since: hold.startedAt,
5857
+ heldRequests: 1,
5858
+ });
5859
+ if (this.budgetHolds.get(agent.name) !== hold) {
5860
+ endGate();
5861
+ return true;
5862
+ }
5863
+ if (!wait.alerted) {
5864
+ wait.alerted = true;
5865
+ this.opsAlert('context-refusal', agent.name, this.budgetHoldOpenText(hold), {
5866
+ skipLog: true,
5867
+ key: String(hold.wait.since),
5868
+ data: {
5869
+ errorType,
5870
+ since: hold.wait.since,
5871
+ ...(hold.budget !== undefined ? { budget: hold.budget } : {}),
5872
+ ...(hold.actual !== undefined ? { actual: hold.actual } : {}),
5873
+ },
5874
+ });
5875
+ if (this.budgetHolds.get(agent.name) !== hold) {
5876
+ endGate();
5877
+ return true;
5878
+ }
5879
+ }
5880
+ endGate();
5881
+ if (this.budgetHolds.get(agent.name) !== hold)
5882
+ return true;
5883
+ this.retractUnsentInjections(agent);
5884
+ if (this.budgetHolds.get(agent.name) !== hold)
5885
+ return true;
5886
+ const readiness = this.contextReadiness(agent.getContextManager());
5887
+ const compression = 'pending' in readiness && readiness.pending ? ` Compression: ${boundFailureText(readiness.pending)}` : '';
5888
+ const again = carried ? `, as of a wait that began ${describeWait(startedAt - carried.since)} ago` : '';
5889
+ console.error(`[budget-hold] agent=${agent.name} compile refused (${errorType}): ${hold.reason} \u2014 Held${again}: ` +
5890
+ `the turn waits, idle, and runs once compression makes room.${compression}`);
5891
+ this.driveBudgetHoldSoon(agent);
5892
+ return true;
5893
+ }
5894
+ rowsTurnStandsOn(agent, turn, routing) {
5895
+ if (!continuesTurn(turn)) {
5896
+ const row = this.activeTurnTriggers.get(agent.name)?.readsRow;
5897
+ return row !== undefined ? [row] : [];
5898
+ }
5899
+ const rows = routing.firstRow !== undefined ? [routing.firstRow] : [];
5900
+ const from = routing.start?.sequence;
5901
+ if (from === undefined)
5902
+ return rows;
5903
+ try {
5904
+ const ownCalls = new Set();
5905
+ for (const row of agent.getContextManager().getPresentedMessages()) {
5906
+ if (!ownWindowRow(row, agent, ownCalls) || row.sequence <= from)
5907
+ continue;
5908
+ if (storedInjectionMarkOf(row.metadata) !== undefined || clockStampOf(row.metadata) !== undefined)
5909
+ continue;
5910
+ rows.push(String(row.id));
5911
+ }
5912
+ }
5913
+ catch (error) {
5914
+ console.error(`[budget-hold] agent=${agent.name}: could not read the rows its held turn stands on, so a surgery checks its first row alone:`, error);
5915
+ }
5916
+ return rows;
5917
+ }
5918
+ compressionPending(agent) {
5919
+ try {
5920
+ return !agent.getContextManager().isReady();
5921
+ }
5922
+ catch {
5923
+ return false;
5924
+ }
5925
+ }
5926
+ startBudgetHoldRecheck(agent, hold) {
5927
+ clearInterval(hold.recheck);
5928
+ hold.recheck = undefined;
5929
+ if (this.maintenanceIntervalMs > 0)
5930
+ return;
5931
+ hold.recheck = setInterval(() => this.driveBudgetHold(agent), this.budgetHoldRecheckMs);
5932
+ hold.recheck.unref?.();
5933
+ }
5934
+ driveBudgetHoldSoon(agent) {
5935
+ const kick = setTimeout(() => this.driveBudgetHold(agent), 0);
5936
+ kick.unref?.();
5937
+ }
5938
+ driveBudgetHold(agent) {
5939
+ const hold = this.budgetHolds?.get(agent.name);
5940
+ if (!hold || this.agents.get(agent.name) !== agent || hold.checkQueued)
5941
+ return;
5942
+ if (this.providerGateBlocked(agent.name))
5943
+ return;
5944
+ const kicked = this.kickOverBudgetDrain(agent.name, agent, () => {
5945
+ hold.checkQueued = false;
5946
+ if (this.budgetHolds.get(agent.name) !== hold)
5947
+ return;
5948
+ if (this.budgetHoldMoved(agent, hold))
5949
+ this.releaseBudgetHold(agent.name);
5950
+ else if (kicked === 'already draining')
5951
+ this.driveBudgetHold(agent);
5952
+ }, 'hold', hold.drainKicked);
5953
+ if (kicked === 'kicking' || kicked === 'already draining')
5954
+ hold.checkQueued = true;
5955
+ if (kicked === 'kicking')
5956
+ hold.drainKicked = true;
5957
+ }
5958
+ releaseBudgetHolds() {
5959
+ for (const name of [...(this.budgetHolds?.keys() ?? [])])
5960
+ this.releaseBudgetHold(name);
5961
+ }
5962
+ releaseBudgetHoldsOnEvidence() {
5963
+ for (const name of [...(this.budgetHolds?.keys() ?? [])])
5964
+ this.releaseBudgetHoldOnEvidence(name);
5965
+ }
5966
+ releaseBudgetHoldOnEvidence(agentName) {
5967
+ const hold = this.budgetHolds?.get(agentName);
5968
+ const agent = this.agents.get(agentName);
5969
+ if (hold && agent && this.budgetHoldMoved(agent, hold))
5970
+ this.releaseBudgetHold(agentName);
5971
+ }
5972
+ budgetHoldMoved(agent, hold) {
5973
+ if (!this.compressionPending(agent) || agent.contextSettingsChanges() !== hold.settingsAtHold)
5974
+ return true;
5975
+ const progress = this.compressionProgressOf(agent);
5976
+ if (progress !== undefined && hold.progressAtHold !== undefined)
5977
+ return progress !== hold.progressAtHold;
5978
+ if (progress !== undefined)
5979
+ hold.progressAtHold = progress;
5980
+ return hold.managerCalls !== 0;
5981
+ }
5982
+ compressionProgressOf(agent) {
5983
+ const progress = this.contextProgress(agent.getContextManager());
5984
+ if (!progress)
5985
+ return undefined;
5986
+ try {
5987
+ const { chunksCompressed, summaryCounts } = progress;
5988
+ if (chunksCompressed === undefined && summaryCounts === undefined)
5989
+ return undefined;
5990
+ return JSON.stringify([chunksCompressed, summaryCounts]);
5991
+ }
5992
+ catch (error) {
5993
+ console.error(`[budget-hold] agent=${agent.name}: could not read its compression progress, so it reads as none:`, error);
5994
+ return undefined;
5995
+ }
5996
+ }
5997
+ closureWaits(agentName) {
5998
+ const closure = (r) => r.agentName === agentName
5999
+ && (mergedBatch(r) ?? [r]).some((m) => m.reason === 'conversation:closure');
6000
+ return this.pendingRequests.some(closure)
6001
+ || [...(this.passRequests ?? [])].some((pass) => (pass.get(agentName) ?? []).some(closure))
6002
+ || (this.providerAccelerationCooldowns.get(agentName)?.heldRequests ?? []).some(closure)
6003
+ || (this.budgetHolds?.get(agentName)?.wakes ?? []).some(closure);
6004
+ }
6005
+ releaseBudgetHold(agentName) {
6006
+ const hold = this.budgetHolds?.get(agentName);
6007
+ const agent = this.agents.get(agentName);
6008
+ if (!hold || !agent || this.quiesced || this.surgeryHold)
6009
+ return;
6010
+ const requests = [hold.turn, ...hold.wakes];
6011
+ hold.wakes = [];
6012
+ this.budgetHolds.delete(agentName);
6013
+ clearInterval(hold.recheck);
6014
+ hold.recheck = undefined;
6015
+ appendAll(this.pendingRequests, requests);
6016
+ console.error(`[budget-hold] agent=${agentName} released after ${Date.now() - hold.startedAt}ms: ` +
6017
+ `${requests.length} request(s) queued, and the start's own compile is the check`);
6018
+ const note = this.storeBudgetHoldNote(agent, hold);
6019
+ if (note !== undefined)
6020
+ this.emitTrace({ type: 'message:added', messageId: note, source: `context-injection:${BUDGET_HOLD_NOTE_NAMESPACE}` });
6021
+ this.finishBudgetHold(agentName, hold, 'released');
6022
+ }
6023
+ storeBudgetHoldNote(agent, hold) {
6024
+ const size = hold.budget !== undefined && hold.actual !== undefined
6025
+ ? ` (${hold.actual.toLocaleString('en-US')} tokens against ${hold.budget.toLocaleString('en-US')})`
6026
+ : '';
6027
+ const text = `[budget-hold] Your context didn't fit its budget${size} when a turn came to start. ` +
6028
+ `That turn waited ${describeWait(Date.now() - hold.wait.since)}.`;
6029
+ const row = asStoredRow({ namespace: BUDGET_HOLD_NOTE_NAMESPACE, position: 'afterUser', content: [{ type: 'text', text }] });
6030
+ const mark = { agent: agent.name, namespace: row.namespace, contentHash: unitHash([row]), part: 0, parts: 1 };
6031
+ try {
6032
+ agent.markUnsentPoint();
6033
+ return storeOwnRow(agent, row.participant, row.content, {
6034
+ system: true, kind: STORED_INJECTION_KIND, [STORED_INJECTION_KEY]: mark,
6035
+ }, undefined, !this.logicalTurnAlive(agent));
6036
+ }
6037
+ catch (error) {
6038
+ console.error(`[budget-hold] agent=${agent.name}: the note for its released turn could not be stored:`, error);
6039
+ return undefined;
6040
+ }
6041
+ }
6042
+ finishBudgetHold(agentName, hold, outcome) {
6043
+ if (this.budgetHolds?.get(agentName) === hold)
6044
+ this.budgetHolds.delete(agentName);
6045
+ clearInterval(hold.recheck);
6046
+ hold.recheck = undefined;
6047
+ const waitedMs = Date.now() - hold.startedAt;
6048
+ console.error(`[budget-hold] agent=${agentName} hold ended (${outcome}) after ${waitedMs}ms`);
6049
+ this.emitTrace({ type: 'inference:hold_ended', agentName, outcome, waitedMs });
6050
+ if (outcome === 'released' || !hold.wait.alerted)
6051
+ return;
6052
+ const waitedFor = Date.now() - hold.wait.since;
6053
+ const after = describeWait(waitedFor);
6054
+ const words = outcome === 'removed'
6055
+ ? `what the waiting turn was for was removed or taken out of its window after ${after}, so its waiting start doesn't run`
6056
+ : `the waiting turn was stopped after ${after}, so its waiting start doesn't run`;
6057
+ this.opsAlert('context-refusal-clear', agentName, words, {
6058
+ skipLog: true,
6059
+ key: String(hold.wait.since),
6060
+ data: { outcome, waitedMs: waitedFor },
6061
+ });
6062
+ }
6063
+ budgetHoldOpenText(hold) {
6064
+ const head = `compile refused (${hold.errorType}): `;
6065
+ const tail = ' \u2014 the turn waits while compression makes room, and runs once its context fits';
6066
+ return head + boundFailureText(hold.reason, FAILURE_TEXT_MAX_CHARS - head.length - tail.length) + tail;
6067
+ }
6068
+ retractUnsentInjections(agent) {
6069
+ if (!this.presents(agent))
6070
+ return;
6071
+ try {
6072
+ const ids = agent.unsentInjectionRows();
6073
+ if (ids.length === 0)
6074
+ return;
6075
+ agent.getContextManager().retractRows(ids, { undone: true });
6076
+ this.emitTrace({ type: 'presentation:retracted', agentName: agent.name, messageIds: ids, reason: 'unsent-injection' });
6077
+ }
6078
+ catch (error) {
6079
+ console.error(`[budget-hold] agent=${agent.name}: the injection rows its refused start stored could not leave its window:`, error);
6080
+ }
6081
+ }
6082
+ endHeldTurn(agentName, turn, drop) {
6083
+ this.endContinuedTurn(agentName, drop, !continuesTurn(turn));
6084
+ }
6085
+ dropBudgetHold(agentName, drop, wakes = 'gone') {
6086
+ const hold = this.budgetHolds?.get(agentName);
6087
+ if (!hold)
6088
+ return false;
6089
+ this.budgetHolds.delete(agentName);
6090
+ clearInterval(hold.recheck);
6091
+ hold.recheck = undefined;
6092
+ if (wakes === 'back')
6093
+ appendAll(this.pendingRequests, hold.wakes);
6094
+ hold.wakes = [];
6095
+ try {
6096
+ this.endHeldTurn(agentName, hold.turn, drop);
6097
+ }
6098
+ finally {
6099
+ this.finishBudgetHold(agentName, hold, 'stopped');
6100
+ }
6101
+ return true;
6102
+ }
6103
+ heldContinuation(agentName) {
6104
+ const hold = this.budgetHolds?.get(agentName);
6105
+ return hold !== undefined && continuesTurn(hold.turn);
6106
+ }
6107
+ dropHeldTurn(agentName, hold, why) {
6108
+ this.endDetachedHold(agentName, this.detachBudgetHold(agentName, hold), why);
6109
+ }
6110
+ /** Take `hold` out of its agent's wait, with no trace yet. */
6111
+ detachBudgetHold(agentName, hold) {
6112
+ this.budgetHolds.delete(agentName);
6113
+ clearInterval(hold.recheck);
6114
+ hold.recheck = undefined;
6115
+ return hold;
6116
+ }
6117
+ /** End a turn detachBudgetHold took out of its wait: its wakes back in the queue, then its end traced. */
6118
+ endDetachedHold(agentName, hold, why) {
6119
+ appendAll(this.pendingRequests, hold.wakes);
6120
+ hold.wakes = [];
6121
+ try {
6122
+ this.endHeldTurn(agentName, hold.turn, { kind: 'dropped', why, reason: BUDGET_HELD_TURN_REMOVED });
6123
+ }
6124
+ finally {
6125
+ this.finishBudgetHold(agentName, hold, 'removed');
6126
+ }
6127
+ }
6128
+ rowGone(agent, row) {
6129
+ try {
6130
+ return agent.getContextManager().getMessage(row) === null;
6131
+ }
6132
+ catch (error) {
6133
+ console.error(`[budget-hold] agent=${agent.name}: could not read whether row ${row} is still there, so what waits on it stays:`, error);
6134
+ return false;
6135
+ }
6136
+ }
6137
+ rowLeftWindow(agent, row) {
6138
+ if (this.rowGone(agent, row))
6139
+ return true;
6140
+ if (!this.presents(agent))
6141
+ return false;
6142
+ try {
6143
+ return agent.getContextManager().ordinalOf(row) === undefined;
6144
+ }
6145
+ catch (error) {
6146
+ console.error(`[budget-hold] agent=${agent.name}: could not read whether row ${row} is still in its window, so what waits on it stays:`, error);
6147
+ return false;
6148
+ }
6149
+ }
6150
+ budgetWaitEnded(agent, trigger) {
6151
+ const row = budgetWaitOf(trigger)?.row;
6152
+ if (row === undefined || !this.rowLeftWindow(agent, row))
6153
+ return false;
6154
+ const batch = mergedBatch(trigger);
6155
+ if (batch)
6156
+ appendAll(this.pendingRequests, batch.filter((r) => budgetWaitOf(r) === undefined));
6157
+ this.endContinuedTurn(agent.name, { kind: 'dropped', why: 'what its held turn stood on was removed', reason: BUDGET_HELD_TURN_REMOVED }, true);
6158
+ return true;
6159
+ }
6160
+ checkBudgetHoldsAfterWindowChange(only, release = true) {
6161
+ for (const [agentName, hold] of [...(this.budgetHolds?.entries() ?? [])]) {
6162
+ const agent = this.agents.get(agentName);
6163
+ if (!agent || (only !== undefined && agentName !== only) || this.budgetHolds?.get(agentName) !== hold)
6164
+ continue;
6165
+ const wakes = hold.wakes.filter((wake) => wake.readsRow === undefined || !this.rowGone(agent, wake.readsRow));
6166
+ if (wakes.length < hold.wakes.length) {
6167
+ console.error(`[budget-hold] agent=${agentName}: ${hold.wakes.length - wakes.length} held wake(s) dropped, their rows removed`);
6168
+ }
6169
+ hold.wakes = wakes;
6170
+ if (hold.standsOn.some((row) => this.rowLeftWindow(agent, row))) {
6171
+ this.dropHeldTurn(agentName, hold, 'a surgery or an undo removed what its held turn stood on');
6172
+ }
6173
+ else if (release) {
6174
+ this.releaseBudgetHold(agentName);
6175
+ }
6176
+ }
6177
+ }
6178
+ kickOverBudgetDrain(agentName, agent, after, by = 'breaker', quiet = false) {
6179
+ // Like a maintenance pass: none starts once stop() has begun, it starts
6180
+ // no tick while a surgery holds the store or once its agent has been
6181
+ // disposed, and a surgery, a switchBranch or stop() waits for the tick
6182
+ // in flight. It joins the drains they wait for before the breaker's alert is
6183
+ // raised and before its body runs, so the alert's listeners find it:
6184
+ // one that begins stop() or a surgery waits for it, and its body then
6185
+ // ticks none and says why. So does a stop() made from inside its first
6186
+ // tick, before that tick's first await.
6187
+ if (this.providerAdmissionClosed)
6188
+ return 'not kicked: the framework is stopping';
6189
+ if (this.surgeryHold)
6190
+ return 'not kicked: a surgery holds the store';
6191
+ if (after) {
6192
+ const waiting = this.overBudgetDrainAfter.get(agentName);
6193
+ if (waiting)
6194
+ waiting.push(after);
6195
+ else
6196
+ this.overBudgetDrainAfter.set(agentName, [after]);
6197
+ }
6198
+ if (this.overBudgetDrainInFlight.has(agentName))
6199
+ return 'already draining';
6200
+ this.overBudgetDrainInFlight.add(agentName);
6201
+ const tag = by === 'hold' ? '[budget-hold]' : '[inference-failed]';
6202
+ const label = by === 'hold' ? 'budget hold' : 'OverBudget breaker';
6203
+ const drain = Promise.resolve().then(async () => {
6204
+ let ticks = 0;
6205
+ let record;
6206
+ try {
6207
+ const cm = agent.getContextManager();
6208
+ if (by === 'hold') {
6209
+ const readiness = this.contextReadiness(cm);
6210
+ const progress = this.contextProgress(cm);
6211
+ record = {
6212
+ agentName, startedAt: Date.now(), ticks: 0, readyBefore: false,
6213
+ ...('pending' in readiness && readiness.pending ? { pendingBefore: readiness.pending } : {}),
6214
+ ...(progress ? { progressBefore: progress } : {}),
6215
+ };
6216
+ }
6217
+ const holding = () => this.budgetHolds.has(agentName);
6218
+ while ((by === 'hold' ? holding() : ticks < 8) && !this.providerAdmissionClosed && !this.surgeryHold && this.agents.get(agentName) === agent) {
6219
+ const calls = by === 'hold' ? this.budgetHolds.get(agentName)?.managerCalls : undefined;
6220
+ await cm.tick();
6221
+ ticks++;
6222
+ if (by === 'breaker')
6223
+ continue;
6224
+ const held = this.budgetHolds.get(agentName);
6225
+ if (held && this.budgetHoldMoved(agent, held)) {
6226
+ this.releaseBudgetHold(agentName);
6227
+ if (this.quiesced)
6228
+ break;
6229
+ }
6230
+ else if (held?.managerCalls === calls || this.compressionProgressOf(agent) !== undefined) {
6231
+ break;
6232
+ }
6233
+ }
6234
+ if (by === 'hold' && ticks > 0)
6235
+ this.maintenanceFailureReported.delete(agentName);
6236
+ const stopped = by === 'breaker' && ticks >= 8 ? ''
6237
+ : this.agents.get(agentName) !== agent ? ', stopped as the agent was disposed'
6238
+ : this.providerAdmissionClosed ? ', stopped as the framework stops'
6239
+ : this.surgeryHold ? ', stopped while a surgery holds the store'
6240
+ : '';
6241
+ if (!quiet || stopped)
6242
+ console.error(`${tag} drain kicked for ${agentName} (${label}, ${ticks} ticks${stopped})`);
6243
+ }
6244
+ catch (err) {
6245
+ if (err instanceof MaintenanceWithdrawnError) {
6246
+ console.error(`${tag} drain kicked for ${agentName} (${label}, ${ticks} ticks, stopped: ${err.message})`);
6247
+ }
6248
+ else if (record) {
6249
+ record.ticks = ticks;
6250
+ record.finishedAt = Date.now();
6251
+ this.reportMaintenanceFailure(agentName, record, err instanceof Error ? String(err.message) : failureDescription(err));
6252
+ }
6253
+ else {
6254
+ console.error(`${tag} drain kick failed for ${agentName} after ${ticks} ticks: ${describeFailureWithFrames(err)}`);
6255
+ }
6256
+ }
6257
+ finally {
6258
+ this.overBudgetDrainInFlight.delete(agentName);
6259
+ }
6260
+ const waiting = this.overBudgetDrainAfter?.get(agentName);
6261
+ if (waiting) {
6262
+ this.overBudgetDrainAfter.delete(agentName);
6263
+ for (const run of waiting)
6264
+ run();
6265
+ }
6266
+ });
6267
+ this.overBudgetDrains.add(drain);
6268
+ void drain.finally(() => { this.overBudgetDrains.delete(drain); });
6269
+ return 'kicking';
6270
+ }
5746
6271
  /** When a hold's timer next fires: at its deadline, stepped past what one
5747
6272
  * timer can hold. A hold with no deadline (a wait held until released, or
5748
6273
  * the stand-in for unreadable records) asks again as often as unreadable
@@ -5803,7 +6328,7 @@ export class AgentFramework {
5803
6328
  }
5804
6329
  const requests = cooldown.heldRequests.length > 0 ? cooldown.heldRequests : [{ agentName,
5805
6330
  reason: 'provider-acceleration-retry', source: 'framework', timestamp: Date.now() }];
5806
- this.pendingRequests.push(...requests);
6331
+ appendAll(this.pendingRequests, requests);
5807
6332
  console.error(cooldown.rearm
5808
6333
  ? `[inference-rearm] agent=${agentName} turn queued again (${cooldown.rearm.number} of ${cooldown.rearm.of})`
5809
6334
  : `[provider-cooldown] agent=${agentName} released after ${Date.now() - cooldown.startedAt}ms — ` +
@@ -5959,7 +6484,7 @@ export class AgentFramework {
5959
6484
  // (releaseProviderWait) before anything was held: nothing binds now, so
5960
6485
  // the requests start, as the release of an installed hold starts them.
5961
6486
  if (report && !this.providerWaits?.active(agent.name, wait.model)) {
5962
- this.pendingRequests.push(...requests);
6487
+ appendAll(this.pendingRequests, requests);
5963
6488
  return true;
5964
6489
  }
5965
6490
  const existing = this.providerAccelerationCooldowns.get(agent.name);
@@ -6152,6 +6677,7 @@ export class AgentFramework {
6152
6677
  void pass.finally(() => {
6153
6678
  if (this.maintenancePass === pass)
6154
6679
  this.maintenancePass = null;
6680
+ this.releaseBudgetHoldsOnEvidence();
6155
6681
  });
6156
6682
  begin(this.runQueuedMaintenance().catch((error) => {
6157
6683
  // The alert cuts its own copies from the whole message, so the
@@ -6658,6 +7184,9 @@ export class AgentFramework {
6658
7184
  this.dropContinuations(agentName, { kind: 'caller', reason, why: 'a stop ended the turn' });
6659
7185
  return true;
6660
7186
  }
7187
+ if (!result && this.budgetHolds?.has(agentName)) {
7188
+ return this.dropBudgetHold(agentName, { kind: 'caller', reason, why: 'a stop ended the turn' }, 'back');
7189
+ }
6661
7190
  return !!result;
6662
7191
  }
6663
7192
  /**
@@ -6762,6 +7291,7 @@ export class AgentFramework {
6762
7291
  log({
6763
7292
  success: false,
6764
7293
  error: `Stream cancelled (${reason})`,
7294
+ errorWords: `Stream cancelled (${reason})`,
6765
7295
  request: compiledRequest ?? { note: 'streaming request aborted' },
6766
7296
  durationMs,
6767
7297
  });
@@ -6829,6 +7359,7 @@ export class AgentFramework {
6829
7359
  log({
6830
7360
  success: false,
6831
7361
  error: `Stream aborted: ${kind}`,
7362
+ errorWords: `Stream aborted: ${kind}`,
6832
7363
  request: compiledRequest ?? { note: `streaming request aborted by ${kind}` },
6833
7364
  durationMs,
6834
7365
  });
@@ -7477,7 +8008,7 @@ export class AgentFramework {
7477
8008
  const extKeys = [];
7478
8009
  for (const ext of extensions.values()) {
7479
8010
  Object.assign(extProperties, ext.properties);
7480
- extKeys.push(...ext.keys);
8011
+ appendAll(extKeys, ext.keys);
7481
8012
  }
7482
8013
  const settingsProp = baseSchema.properties.settings;
7483
8014
  return {
@@ -7842,7 +8373,8 @@ export class AgentFramework {
7842
8373
  ...this.pendingRequests,
7843
8374
  ...[...this.passRequests].flatMap((pass) => [...pass.values()].flat()),
7844
8375
  ...[...this.providerAccelerationCooldowns.values()].flatMap((c) => c.heldRequests),
7845
- ].flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length,
8376
+ ].flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length
8377
+ + [...(this.budgetHolds?.values() ?? [])].reduce((n, h) => n + 1 + h.wakes.flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length, 0),
7846
8378
  backgroundScripts: this.codeExecution?.scripts.leftRunning() ?? 0,
7847
8379
  // Deprecated (#291): no queue is kept. What a live surgery's hold
7848
8380
  // keeps in memory, to land at its release, and what waits in memory
@@ -8207,6 +8739,7 @@ export class AgentFramework {
8207
8739
  console.error(this.queue?.isClosed
8208
8740
  ? `[host-mode] resumed — ${releasedRequests} parked wake(s) won't run because the framework is stopping`
8209
8741
  : `[host-mode] resumed — ${releasedRequests} parked wake(s) released`);
8742
+ this.releaseBudgetHolds();
8210
8743
  }
8211
8744
  return this.getHostModeStatus();
8212
8745
  }
@@ -9648,10 +10181,12 @@ export class AgentFramework {
9648
10181
  store: this.store,
9649
10182
  namespace,
9650
10183
  isolate: true,
10184
+ viewFilter: (message) => !isReasoningLoss(message.metadata),
9651
10185
  strategy: config.strategy ?? new PassthroughStrategy(),
9652
10186
  membrane: this.ephemeralAuxiliaryMembraneFor(config.name, release),
9653
10187
  debugLogContext: !!process.env.DEBUG_CONTEXT,
9654
10188
  });
10189
+ this.storeSees.set(contextManager, (metadata) => !isReasoningLoss(metadata));
9655
10190
  ownSlot = { namespace };
9656
10191
  }
9657
10192
  // Either manager is the run's to release (main's manager release), the derived one included.
@@ -10035,7 +10570,8 @@ export class AgentFramework {
10035
10570
  * The returned manager uses a passthrough strategy, so nothing is
10036
10571
  * folded: its compile is the agent's window as the agent's view shows it
10037
10572
  * (the rows the window holds, through its view filter), and in legacy
10038
- * mode, where the window is the record, the record so. getAllMessages()
10573
+ * mode, where the window is the record, the record so. These compiled
10574
+ * views omit legacy reasoning-loss notices. getAllMessages()
10039
10575
  * lists the record. An ended agent's context is the real one — treat it as
10040
10576
  * read-only unless you mean to change what a later resume starts from.
10041
10577
  */
@@ -10154,9 +10690,9 @@ export class AgentFramework {
10154
10690
  * and what it observes (context-options.ts): a resident's, on the
10155
10691
  * residents' slot, or for an isolated agent that watches that slot, the
10156
10692
  * residents' view when its edge leaves held traffic out, and the
10157
- * subconscious's when it shows it. Undefined for an isolated agent that
10693
+ * subconscious's when it shows it. For an isolated agent that
10158
10694
  * watches no residents' slot on its branch (a worker, a conversation
10159
- * fork), whose context was opened with no filter.
10695
+ * fork), whose view excludes only legacy reasoning-loss notices.
10160
10696
  */
10161
10697
  liveViewFilterOf(record) {
10162
10698
  if (record.ownSlot?.namespace === null)
@@ -10166,7 +10702,7 @@ export class AgentFramework {
10166
10702
  return slot !== undefined && slot.branch === undefined && sameSlot(slot, RESIDENTS_SLOT);
10167
10703
  });
10168
10704
  if (residents.length === 0)
10169
- return undefined;
10705
+ return (message) => !isReasoningLoss(message.metadata);
10170
10706
  return residents.some((edge) => edge.includeHeld === true) ? subconsciousViewFilter(record.name) : residentViewFilter(record.name);
10171
10707
  }
10172
10708
  /**
@@ -10192,7 +10728,7 @@ export class AgentFramework {
10192
10728
  * (deriveSharedContext): what the agent its chain of derivations started
10193
10729
  * from sees (liveViewFilterOf), and the stored injections of each agent in
10194
10730
  * the chain, which carry that agent's name and would otherwise read as
10195
- * another's. Undefined when the owner's context has no filter.
10731
+ * another's. Legacy notices remain excluded even for an own injection.
10196
10732
  */
10197
10733
  derivedViewFilterOf(record) {
10198
10734
  const owner = this.viewOwnerOf(record);
@@ -10211,7 +10747,8 @@ export class AgentFramework {
10211
10747
  }
10212
10748
  if (names.length === 0)
10213
10749
  return ownerSees;
10214
- return (message) => ownerSees(message) || names.some((name) => isOwnInjection(message.metadata, name));
10750
+ return (message) => !isReasoningLoss(message.metadata)
10751
+ && (ownerSees(message) || names.some((name) => isOwnInjection(message.metadata, name)));
10215
10752
  }
10216
10753
  /** Open a derived agent's context on a branch of its own (see deriveAgent). */
10217
10754
  async deriveSharedContext(config, inherit, release) {
@@ -10243,13 +10780,11 @@ export class AgentFramework {
10243
10780
  // reads through it, and is recorded beside the manager (storeSees), so
10244
10781
  // main's checks of whether an agent sees a row (agentSees: a tool
10245
10782
  // boundary's injection, an arrival, a reader of an unread message) ask
10246
- // what its compile shows. A source with no view recorded has no filter
10247
- // (an isolated run's): it sees every row, so its fork keeps none, and
10248
- // sees its own.
10783
+ // what its compile shows. The global legacy-notice exclusion applies
10784
+ // even when the source has no additional view recorded.
10249
10785
  const sourceSees = this.storeSees.get(source);
10250
- const sees = sourceSees
10251
- ? (metadata) => sourceSees(metadata) || isOwnInjection(metadata, config.name)
10252
- : undefined;
10786
+ const sees = (metadata) => !isReasoningLoss(metadata)
10787
+ && (!sourceSees || sourceSees(metadata) || isOwnInjection(metadata, config.name));
10253
10788
  const contextManager = await source.derive({
10254
10789
  branch: ownBranch,
10255
10790
  strategy: config.strategy ?? new PassthroughStrategy(),
@@ -10259,11 +10794,10 @@ export class AgentFramework {
10259
10794
  solve: inherit.solve ?? 'reuse',
10260
10795
  inherit: { refusals: inherit.refusals === true },
10261
10796
  ...(inherit.atSequence !== undefined ? { atSequence: inherit.atSequence } : {}),
10262
- ...(sees ? { viewFilter: (message) => sees(message.metadata) } : {}),
10797
+ viewFilter: (message) => sees(message.metadata),
10263
10798
  debugLogContext: !!process.env.DEBUG_CONTEXT,
10264
10799
  });
10265
- if (sees)
10266
- this.storeSees.set(contextManager, sees);
10800
+ this.storeSees.set(contextManager, sees);
10267
10801
  const derivation = contextManager.getDerivation?.();
10268
10802
  return {
10269
10803
  contextManager,
@@ -10781,6 +11315,7 @@ export class AgentFramework {
10781
11315
  this.closingConversationAgents.delete(name);
10782
11316
  // A continuation of its turn still on its way goes with it.
10783
11317
  this.dropContinuations(name, { kind: 'dropped', why });
11318
+ this.dropBudgetHold(name, { kind: 'dropped', why });
10784
11319
  // A batch its guard still holds is settled as `aborted`: the stream's
10785
11320
  // own finally does neither once the agent has gone.
10786
11321
  if (agent.toolResultGuard.hasPending) {
@@ -11484,9 +12019,10 @@ export class AgentFramework {
11484
12019
  if (outcome.stopReason === 'exhausted') {
11485
12020
  // The caller may log or relay the message, which can echo the whole request.
11486
12021
  const message = boundFailureText(outcome.error ?? 'Unknown error');
12022
+ const options = outcome.cause !== undefined ? { cause: boundedCause(outcome.cause) } : undefined;
11487
12023
  run.settle.reject(outcome.statedWaitMs === undefined
11488
- ? new Error(message)
11489
- : new ProviderWaitNotSleptError(message, outcome.statedWaitMs));
12024
+ ? new Error(message, options)
12025
+ : new ProviderWaitNotSleptError(message, outcome.statedWaitMs, options));
11490
12026
  return;
11491
12027
  }
11492
12028
  run.settle.resolve({
@@ -11504,7 +12040,19 @@ export class AgentFramework {
11504
12040
  * Rejects with ProviderWaitNotSleptError when the run ends rather than sleep
11505
12041
  * a retryable failure's stated wait longer than the framework's ten-minute
11506
12042
  * cap, or one it can't represent, and with a plain Error for any other
11507
- * ending, a stated wait its error policy declines among them.
12043
+ * ending, a stated wait its error policy declines among them. A failed
12044
+ * turn's rejection never carries a provider's message: a provider's failure
12045
+ * is told by membrane's type and the HTTP status, a safety failure only as
12046
+ * a request that couldn't be completed, a progress bound's cut, a stream's
12047
+ * abort and context-manager's refusal of a compile in their own words, and
12048
+ * any other failure as the turn failing. Where an error ended the turn, the
12049
+ * rejection's `cause` is a bounded copy of it: a plain `Error` with its
12050
+ * `name`, its message and stack cut to 2,000 characters, and, from a
12051
+ * MembraneError, its `type`, `retryable`, `httpStatus`, `providerErrorCode`
12052
+ * and `retryAfterMs`, which a host reads by name. It carries no `rawError`
12053
+ * or `rawRequest`, and `instanceof MembraneError` doesn't hold on it. Its
12054
+ * message and stack are the failure's own, a provider's words included, for
12055
+ * classifying the failure and for logs, and aren't for showing to an agent.
11508
12056
  *
11509
12057
  * The caller provides a pre-created agent + contextManager (from createEphemeralAgent).
11510
12058
  * The task message should already be in the context manager.
@@ -12518,7 +13066,7 @@ export class AgentFramework {
12518
13066
  const turnMembers = all.slice(turn.from, turn.to + 1);
12519
13067
  if (turnMembers.some((m) => hasBlock(m, 'tool_use'))) {
12520
13068
  from = turn.from;
12521
- removedIds.push(...turnMembers.map((m) => m.id).reverse());
13069
+ appendAll(removedIds, turnMembers.map((m) => m.id).reverse());
12522
13070
  heads.unshift(all[turn.from].id);
12523
13071
  }
12524
13072
  }
@@ -12557,16 +13105,9 @@ export class AgentFramework {
12557
13105
  const cm = agent.getContextManager();
12558
13106
  const ep = this.rewindEpisode.get(agent.name);
12559
13107
  const count = (ep?.count ?? 0) + 1;
12560
- const why = cause === 'refusal'
12561
- ? `the model refused on them (content filter: ${failureShown(category)}). Their content is ` +
12562
- `not reproduced here (so this note can't re-trigger the filter); the ` +
12563
- `originals remain in the raw record`
12564
- : `the model API kept refusing the requests that carried them (${failureShown(category)}), ` +
12565
- `so that the conversation could go on. Their content is not reproduced ` +
12566
- `here (so this note can't trigger the same refusal); the originals remain ` +
12567
- `in the raw record`;
12568
13108
  const text = `[${NOTICE_TAG.refusalRewind}] The ${count} most recent turn(s) were set aside because ` +
12569
- `${why}. You are clear to continue — if you ` +
13109
+ `the requests that carried them couldn't be completed. Their content isn't shown here, ` +
13110
+ `and the originals remain in the raw record. You are clear to continue — if you ` +
12570
13111
  `were mid-task, take a different approach; otherwise carry on with whatever ` +
12571
13112
  `is now in front of you, or briefly acknowledge the gap and ask what's next.`;
12572
13113
  const blocks = [{ type: 'text', text }];
@@ -12614,9 +13155,9 @@ export class AgentFramework {
12614
13155
  : '';
12615
13156
  const content = success
12616
13157
  ? `🔧 Unstuck **${agentName}** — shed ${n} turn(s); the model responded.${list}`
12617
- : `⚠️ Couldn't unstick **${agentName}** — still refusing after ${n} rewind(s)` +
12618
- `${category ? ` (category=${category})` : ''}.${list}`;
12619
- console.error(`[unstick] agent=${agentName} ${success ? 'succeeded' : 'gave up'} after ${n} rewind(s)`);
13158
+ : `⚠️ Couldn't unstick **${agentName}** — its requests still couldn't be completed after ${n} rewind(s).${list}`;
13159
+ console.error(`[unstick] agent=${agentName} ${success ? 'succeeded' : 'gave up'} after ${n} rewind(s)` +
13160
+ `${category ? ` category=${logValue(category)}` : ''}`);
12620
13161
  if (s.channelId)
12621
13162
  void this.postToChannel(s.serverId, s.channelId, content);
12622
13163
  }
@@ -13227,6 +13768,7 @@ export class AgentFramework {
13227
13768
  requestId: entry.requestId,
13228
13769
  success: entry.success,
13229
13770
  error: entry.error,
13771
+ errorWords: entry.errorWords,
13230
13772
  durationMs: entry.durationMs,
13231
13773
  tokenUsage: entry.tokenUsage,
13232
13774
  stopReason: entry.stopReason,
@@ -13543,7 +14085,7 @@ export class AgentFramework {
13543
14085
  let failures = [];
13544
14086
  try {
13545
14087
  this.subconsciousAnchor?.branchMoved(this.tuneOutCoordinator?.activeEpochIds());
13546
- this.tuneOutCoordinator?.branchMoved();
14088
+ this.tuneOutCoordinator?.branchMoved(sameBranch);
13547
14089
  if (!sameBranch)
13548
14090
  this.redoStacks.clear();
13549
14091
  failures = await this.reinitializeManagers(except);
@@ -13652,6 +14194,22 @@ export class AgentFramework {
13652
14194
  * recorded with its error.
13653
14195
  */
13654
14196
  undoOnWindow(agent, requester) {
14197
+ // A turn under way that waits for compression stands on what the undo
14198
+ // takes out. It leaves its wait first, so the undo finds the agent idle,
14199
+ // and it ends once the undo has kept its record, so a listener of its
14200
+ // end finds the undo done.
14201
+ const held = this.budgetHolds?.get(agent.name);
14202
+ const taken = held && continuesTurn(held.turn) && !this.hasTurn(agent.name) && !this.turnAlive(agent)
14203
+ ? this.detachBudgetHold(agent.name, held) : undefined;
14204
+ try {
14205
+ return this.undoTurnOnWindow(agent, requester);
14206
+ }
14207
+ finally {
14208
+ if (taken)
14209
+ this.endDetachedHold(agent.name, taken, 'an undo took out what its held turn stood on');
14210
+ }
14211
+ }
14212
+ undoTurnOnWindow(agent, requester) {
13655
14213
  const agentName = agent.name;
13656
14214
  // A turn alive (its start, its stream, its tools, a continuation or a
13657
14215
  // provider hold's retry on its way) holds the window: its next rows
@@ -13695,6 +14253,7 @@ export class AgentFramework {
13695
14253
  }
13696
14254
  this.emitTrace({ type: 'window:undone', agentName, retracted: 0 });
13697
14255
  record(window);
14256
+ this.checkBudgetHoldsAfterWindowChange(agentName);
13698
14257
  return { undone: true, turnIndex: checkpoint.turnIndex, fromBranch: branch, toBranch: branch, window };
13699
14258
  }
13700
14259
  const entry = {
@@ -13727,6 +14286,7 @@ export class AgentFramework {
13727
14286
  const reason = error instanceof Error ? error.message : String(error);
13728
14287
  this.emitTrace({ type: 'window:undone', agentName, retracted: left });
13729
14288
  record({ retracted: left, represented: 0 }, reason);
14289
+ this.checkBudgetHoldsAfterWindowChange(agentName, false);
13730
14290
  throw new OperatorActionError('failed', `Undo of ${agentName}'s turn ${checkpoint.turnIndex} was cut short: ${left} of the ${tail.length} rows it takes out of the window left it ` +
13731
14291
  `(${reason}). redo() puts them back in their order, or another undo finishes it`, { cause: error });
13732
14292
  }
@@ -13747,12 +14307,14 @@ export class AgentFramework {
13747
14307
  const reason = error instanceof Error ? error.message : String(error);
13748
14308
  this.emitTrace({ type: 'window:undone', agentName, retracted: tail.length });
13749
14309
  record(window, reason);
14310
+ this.checkBudgetHoldsAfterWindowChange(agentName, false);
13750
14311
  const rows = tail.length === 1 ? '1 row' : `${tail.length} rows`;
13751
14312
  throw new OperatorActionError('failed', `Undo of ${agentName}'s turn ${checkpoint.turnIndex} took its ${rows} out of the window, and saving its checkpoints ` +
13752
14313
  `failed (${reason}). redo() puts ${tail.length === 1 ? 'it' : 'them'} back, or another undo finishes it`, { cause: error });
13753
14314
  }
13754
14315
  this.emitTrace({ type: 'window:undone', agentName, retracted: tail.length });
13755
14316
  record(window);
14317
+ this.checkBudgetHoldsAfterWindowChange(agentName);
13756
14318
  return { undone: true, turnIndex: checkpoint.turnIndex, fromBranch: branch, toBranch: branch, window };
13757
14319
  }
13758
14320
  /**
@@ -13949,7 +14511,7 @@ export class AgentFramework {
13949
14511
  this.retakeMarks(agent, cm, entry, back, below);
13950
14512
  const ordinals = back.map((id) => cm.ordinalOf(id)).filter((ordinal) => ordinal !== undefined);
13951
14513
  const boundary = {
13952
- indexLengthBefore: ordinals.length > 0 ? Math.min(...ordinals) : cm.indexLength(),
14514
+ indexLengthBefore: ordinals.length > 0 ? ordinals.reduce((min, ordinal) => Math.min(min, ordinal), Infinity) : cm.indexLength(),
13953
14515
  windowBranch: cm.currentBranch().name,
13954
14516
  };
13955
14517
  const checkpoints = this.getTurnCheckpoints(agentName);
@@ -14235,6 +14797,7 @@ export class AgentFramework {
14235
14797
  this.releaseSurgeryHeldWrites();
14236
14798
  for (const name of reserved.keys())
14237
14799
  this.presentAtEndOf(this.agents.get(name));
14800
+ this.checkBudgetHoldsAfterWindowChange();
14238
14801
  };
14239
14802
  }
14240
14803
  async reinitializeManagers(except) {
@@ -14294,7 +14857,7 @@ export class AgentFramework {
14294
14857
  }
14295
14858
  console.error(`[surgery] agent=${agentName}: its context manager is re-initialized, so the ${writes.length} write(s) kept in ` +
14296
14859
  "memory for it while it refused its work go to the move's release, ahead of the writes its hold keeps");
14297
- recovered.push(...writes);
14860
+ appendAll(recovered, writes);
14298
14861
  }
14299
14862
  if (recovered.length === 0)
14300
14863
  return;
@@ -14302,7 +14865,9 @@ export class AgentFramework {
14302
14865
  // two agents whose slot one record holds (residents) land there in the
14303
14866
  // order they came.
14304
14867
  recovered.sort((x, y) => x.heldOrder - y.heldOrder);
14305
- this.surgeryHeldWrites.unshift(...recovered);
14868
+ const later = this.surgeryHeldWrites.splice(0);
14869
+ appendAll(this.surgeryHeldWrites, recovered);
14870
+ appendAll(this.surgeryHeldWrites, later);
14306
14871
  if (!this.surgeryHold)
14307
14872
  this.releaseSurgeryHeldWrites();
14308
14873
  }
@@ -14365,7 +14930,7 @@ export class AgentFramework {
14365
14930
  return found;
14366
14931
  };
14367
14932
  for (const [agentName, writes] of this.heldForRecovery ?? []) {
14368
- entry(agentName).recovery.push(...writes.filter((write) => !told.has(write)));
14933
+ appendAll(entry(agentName).recovery, writes.filter((write) => !told.has(write)));
14369
14934
  }
14370
14935
  for (const write of this.surgeryHeldWrites ?? []) {
14371
14936
  if (told.has(write))
@@ -15677,10 +16242,19 @@ export class AgentFramework {
15677
16242
  const checkpoints = this.getTurnCheckpoints(agentName);
15678
16243
  checkpoints.push(checkpoint);
15679
16244
  // Trim to max depth
15680
- if (checkpoints.length > MAX_TURN_CHECKPOINTS) {
15681
- checkpoints.splice(0, checkpoints.length - MAX_TURN_CHECKPOINTS);
15682
- }
16245
+ const evicted = checkpoints.length > MAX_TURN_CHECKPOINTS
16246
+ ? checkpoints.splice(0, checkpoints.length - MAX_TURN_CHECKPOINTS)
16247
+ : [];
15683
16248
  this.saveTurnCheckpoints(agentName, checkpoints);
16249
+ return { turnIndex, evicted };
16250
+ }
16251
+ dropTurnCheckpoint(agentName, recorded) {
16252
+ const checkpoints = this.getTurnCheckpoints(agentName);
16253
+ if (checkpoints.at(-1)?.turnIndex !== recorded.turnIndex)
16254
+ return;
16255
+ checkpoints.pop();
16256
+ this.saveTurnCheckpoints(agentName, recorded.evicted.concat(checkpoints));
16257
+ this.turnCounters.set(agentName, recorded.turnIndex);
15684
16258
  }
15685
16259
  /**
15686
16260
  * Checkpoints live in one Tree state (TURN_CHECKPOINTS_TREE_ID) keyed by agent
@@ -16560,7 +17134,7 @@ export class AgentFramework {
16560
17134
  // reads the time it's written (clock-rows.ts).
16561
17135
  const clock = injects ? this.storeDueClock(agent, 'tool-boundary') : undefined;
16562
17136
  if (presentsWindow) {
16563
- heard.push(...this.presentation.presentAtBoundary(agent, [replyId, resultsId, clock?.id], { injects }).heard);
17137
+ appendAll(heard, this.presentation.presentAtBoundary(agent, [replyId, resultsId, clock?.id], { injects }).heard);
16564
17138
  }
16565
17139
  // The clock's store and the boundary's presentations run their
16566
17140
  // listeners synchronously too, and one may have stopped the turn,
@@ -16849,6 +17423,7 @@ export class AgentFramework {
16849
17423
  timestamp: Date.now(),
16850
17424
  suppressProse: this.activeTurnTriggers.get(agent.name)?.suppressProse,
16851
17425
  silentHeartbeat: this.activeTurnTriggers.get(agent.name)?.silentHeartbeat,
17426
+ ...this.wakeChannelsMark(this.turnRouting.get(agent.name)),
16852
17427
  }, currentState.stream ? this.streamCarries.get(currentState.stream)?.() : undefined);
16853
17428
  this.pendingRequests.push(continuation);
16854
17429
  // Context budget exceeded: break the stream, let compile() compress.
@@ -16986,6 +17561,7 @@ export class AgentFramework {
16986
17561
  timestamp: Date.now(),
16987
17562
  suppressProse: this.activeTurnTriggers.get(agent.name)?.suppressProse,
16988
17563
  silentHeartbeat: this.activeTurnTriggers.get(agent.name)?.silentHeartbeat,
17564
+ ...this.wakeChannelsMark(this.turnRouting.get(agent.name)),
16989
17565
  });
16990
17566
  }
16991
17567
  }
@@ -17759,6 +18335,7 @@ export class AgentFramework {
17759
18335
  store: this.store,
17760
18336
  namespace: `conversations/${name}`,
17761
18337
  isolate: true,
18338
+ viewFilter: (message) => !isReasoningLoss(message.metadata),
17762
18339
  // Strategy instances are stateful — never share the template's.
17763
18340
  strategy: router.strategyFactory?.() ?? new PassthroughStrategy(),
17764
18341
  // Provider cooldown ownership belongs to the persistent resident only;
@@ -17769,6 +18346,7 @@ export class AgentFramework {
17769
18346
  });
17770
18347
  release.manager = contextManager;
17771
18348
  this.trackManager(contextManager);
18349
+ this.storeSees.set(contextManager, (metadata) => !isReasoningLoss(metadata));
17772
18350
  // Seed with the template's compiled context, renaming the template
17773
18351
  // participant so the fork reads its inheritance as its own history.
17774
18352
  // Guard: only seed a genuinely fresh namespace. Generation counters are
@@ -18027,6 +18605,7 @@ export class AgentFramework {
18027
18605
  this.thinkingLossIncidents.forget(agentName);
18028
18606
  // A continuation of its turn still on its way goes with it.
18029
18607
  this.dropContinuations(agentName, { kind: 'dropped', why: 'its agent was disposed' });
18608
+ this.dropBudgetHold(agentName, { kind: 'dropped', why: 'its agent was disposed' });
18030
18609
  this.turnRouting.delete(agentName);
18031
18610
  this.proseSpeechChains.delete(agentName);
18032
18611
  this.openLogical.delete(agentName);
@@ -18125,8 +18704,8 @@ export class AgentFramework {
18125
18704
  }
18126
18705
  // Fallback reaper: a closing fork whose closure inference never ran
18127
18706
  // (request dropped as stale, inference policy veto) would otherwise stay
18128
- // registered forever. No turn, no active stream and no pending request =
18129
- // done. A turn holds its token from dequeue, through its hooks and
18707
+ // registered forever. No turn, no active stream, no pending request, no
18708
+ // closure wake waiting and no budget hold = done. A turn holds its token from dequeue, through its hooks and
18130
18709
  // compile, before its stream registers, so a fork mid-compile is busy;
18131
18710
  // and a turn is alive while its continuation or a provider hold's retry
18132
18711
  // of it is on its way (hasTurn), so a closure turn held for its retry
@@ -18134,7 +18713,9 @@ export class AgentFramework {
18134
18713
  for (const agentName of [...this.closingConversationAgents]) {
18135
18714
  if (!this.hasTurn(agentName) &&
18136
18715
  !this.activeStreams.has(agentName) &&
18137
- !this.pendingRequests.some((r) => r.agentName === agentName)) {
18716
+ !this.pendingRequests.some((r) => r.agentName === agentName) &&
18717
+ !this.closureWaits(agentName) &&
18718
+ !this.budgetHolds?.has(agentName)) {
18138
18719
  this.disposeConversationAgent(agentName, 'idle-ttl');
18139
18720
  }
18140
18721
  }
@@ -19054,7 +19635,7 @@ export class AgentFramework {
19054
19635
  eventType: 'mcpl:push-event',
19055
19636
  ...(occ.tags ? { tags: occ.tags } : {}),
19056
19637
  },
19057
- }, { suppressible, ...remembered });
19638
+ }, { suppressible, ...remembered, folds: true });
19058
19639
  // Its batch's row, written later, reads this decision, since a rule's
19059
19640
  // script answers once per event. (A copy of the occurrence, as a wake to
19060
19641
  // read a row has, was decided before it was copied.)
@@ -19131,6 +19712,8 @@ export class AgentFramework {
19131
19712
  unrecord(r);
19132
19713
  for (const cooldown of this.providerAccelerationCooldowns.values())
19133
19714
  cooldown.heldRequests.forEach(unrecord);
19715
+ for (const hold of this.budgetHolds?.values() ?? [])
19716
+ hold.wakes.forEach(unrecord);
19134
19717
  for (const r of this.parkedContinuations ?? [])
19135
19718
  unrecord(r);
19136
19719
  for (const pass of this.passRequests ?? [])
@@ -19144,13 +19727,15 @@ export class AgentFramework {
19144
19727
  if (held > 0 && cooldown.heldRequests.length === 0)
19145
19728
  cooldown.emptiedBy = 'withdrawal';
19146
19729
  }
19730
+ for (const hold of this.budgetHolds?.values() ?? [])
19731
+ hold.wakes = hold.wakes.filter((r) => !withdrawn(r));
19147
19732
  for (const pass of this.passRequests ?? []) {
19148
19733
  for (const requests of pass.values()) {
19149
19734
  if (!requests.some((r) => emptied.has(r)))
19150
19735
  continue;
19151
19736
  const left = requests.filter((r) => !emptied.has(r));
19152
19737
  requests.length = 0;
19153
- requests.push(...left);
19738
+ appendAll(requests, left);
19154
19739
  }
19155
19740
  }
19156
19741
  for (const start of emptied) {
@@ -19695,7 +20280,7 @@ export class AgentFramework {
19695
20280
  const agent = this.agents.get(agentName);
19696
20281
  if (!agent) {
19697
20282
  // Agent not found — request is orphaned. Emit warning and drop.
19698
- const oldest = Math.min(...requests.map(r => r.timestamp));
20283
+ const oldest = requests.reduce((min, r) => Math.min(min, r.timestamp), Infinity);
19699
20284
  this.emitTrace({
19700
20285
  type: 'inference:request_dropped',
19701
20286
  agentName,
@@ -19710,6 +20295,12 @@ export class AgentFramework {
19710
20295
  }
19711
20296
  continue;
19712
20297
  }
20298
+ const budgetHold = this.budgetHolds?.get(agentName);
20299
+ if (budgetHold) {
20300
+ for (const req of requests)
20301
+ this.holdRequest(budgetHold.wakes, req);
20302
+ continue;
20303
+ }
19713
20304
  const providerCooldown = this.providerAccelerationCooldowns?.get(agentName);
19714
20305
  if (providerCooldown) {
19715
20306
  // A provider wait binds its own model; with a different primary model
@@ -19746,7 +20337,24 @@ export class AgentFramework {
19746
20337
  // passes, and the read check and the policy decide on them all; the
19747
20338
  // merge below records the batch anew.
19748
20339
  if (requests.some((r) => mergedBatch(r) !== undefined)) {
19749
- requests = requests.flatMap((r) => mergedBatch(r) ?? [r]);
20340
+ requests = requests.flatMap((r) => {
20341
+ const batch = mergedBatch(r);
20342
+ if (!batch?.length)
20343
+ return [r];
20344
+ const wait = budgetWaitOf(r);
20345
+ const waitMarked = wait !== undefined && !batch.some((m) => budgetWaitOf(m) !== undefined);
20346
+ if (waitMarked && !batch.some((m) => this.sameInferenceRequest(m, r))) {
20347
+ const { [MERGED_BATCH]: _batch, ...folded } = r;
20348
+ return [...batch, folded];
20349
+ }
20350
+ const own = r.wakeChannels?.filter((c) => !batch.some((m) => m.wakeChannels?.some((x) => sameChannel(x, c))));
20351
+ const channels = own?.length ? wakeChannelsOf([batch[0], { wakeChannels: own }]) : undefined;
20352
+ const members = channels ? [{ ...batch[0], wakeChannels: channels }, ...batch.slice(1)] : batch;
20353
+ if (!waitMarked)
20354
+ return members;
20355
+ const at = members.findIndex((m) => this.sameInferenceRequest(m, r));
20356
+ return members.map((m, i) => (i === at ? { ...m, [BUDGET_WAIT]: wait } : m));
20357
+ });
19750
20358
  }
19751
20359
  // A provider's stated wait on this agent's primary model, including one
19752
20360
  // recorded before a restart, holds the turn until it passes. An
@@ -19815,7 +20423,7 @@ export class AgentFramework {
19815
20423
  // A heartbeat's wake keeps its own key, so a later channel-less wake
19816
20424
  // can't absorb it: resumed together, they make a heartbeat's batch
19817
20425
  // (heartbeatTurn), as they would have without the quiesce.
19818
- const key = `${request.reason}|${request.addressed ? 'a' : 'n'}|${isHeartbeatWake(request) ? 'h' : ''}|${request.coalescingSubject ?? ''}|${request.readsRow ?? ''}`;
20426
+ const key = `${request.reason}|${request.addressed ? 'a' : 'n'}|${isHeartbeatWake(request) ? 'h' : ''}|${isUnprompted(request) ? 'u' : ''}|${request.coalescingSubject ?? ''}|${request.readsRow ?? ''}|${budgetWaitOf(request) ? 'waited' : ''}`;
19819
20427
  const prev = newestByKey.get(key);
19820
20428
  const kept = !prev || request.timestamp >= prev.timestamp ? request : prev;
19821
20429
  // The wake it absorbs hands on its lines (WakeLine): each event's
@@ -19823,16 +20431,20 @@ export class AgentFramework {
19823
20431
  const lines = prev ? mergeWakeLines([prev, request]) : undefined;
19824
20432
  if (lines)
19825
20433
  kept.wakeLines = lines;
20434
+ const pair = prev ? [prev, request] : undefined;
20435
+ const channels = pair ? wakeChannelsOf(pair) : undefined;
20436
+ if (channels)
20437
+ kept.wakeChannels = channels;
19826
20438
  newestByKey.set(key, kept);
19827
20439
  }
19828
- this.pendingRequests.push(...newestByKey.values());
20440
+ appendAll(this.pendingRequests, [...newestByKey.values()]);
19829
20441
  if (passThrough.length === 0)
19830
20442
  continue;
19831
20443
  // The continuation continues its held turn; its parked siblings must
19832
20444
  // NOT be consumed by that turn — they were re-parked above, so narrow
19833
20445
  // the batch to the continuation(s) alone before the trigger selection.
19834
20446
  requests.length = 0;
19835
- requests.push(...passThrough);
20447
+ appendAll(requests, passThrough);
19836
20448
  }
19837
20449
  // A batch that carries a continuation which resumes its logical turn
19838
20450
  // (resumesLogicalTurn: a budget restart, a tool-result guard's retry)
@@ -19849,10 +20461,10 @@ export class AgentFramework {
19849
20461
  if (requests.some((r) => resumesLogicalTurn(r.reason))) {
19850
20462
  const siblings = requests.filter((r) => !isTurnContinuation(r.reason));
19851
20463
  if (siblings.length > 0) {
19852
- this.pendingRequests.push(...siblings);
20464
+ appendAll(this.pendingRequests, siblings);
19853
20465
  const continuations = requests.filter((r) => isTurnContinuation(r.reason));
19854
20466
  requests.length = 0;
19855
- requests.push(...continuations);
20467
+ appendAll(requests, continuations);
19856
20468
  }
19857
20469
  }
19858
20470
  // Turn-alive includes a continuation of the agent's turn still on its
@@ -19874,7 +20486,7 @@ export class AgentFramework {
19874
20486
  // Re-queue requests, but warn if they've been pending too long.
19875
20487
  // Suppressed while quiesced: a drain-phase busy requeue is expected,
19876
20488
  // not a wedge tell.
19877
- const oldest = Math.min(...requests.map(r => r.timestamp));
20489
+ const oldest = requests.reduce((min, r) => Math.min(min, r.timestamp), Infinity);
19878
20490
  if (!this.quiesced &&
19879
20491
  now - oldest > STALE_REQUEST_MS &&
19880
20492
  (this.staleWarnAt.get(agentName) ?? 0) < now - 60_000) {
@@ -19902,7 +20514,7 @@ export class AgentFramework {
19902
20514
  console.error(`[inference-stale] agent=${agentName} busy (${shownStatus}) — ` +
19903
20515
  `${requests.length} request(s) waiting ${Math.round((now - oldest) / 1000)}s`);
19904
20516
  }
19905
- this.pendingRequests.push(...requests);
20517
+ appendAll(this.pendingRequests, requests);
19906
20518
  continue;
19907
20519
  }
19908
20520
  // A wake whose sole cause was a stored row (readsRow): one not
@@ -19930,7 +20542,7 @@ export class AgentFramework {
19930
20542
  if (read.length > 0) {
19931
20543
  const left = requests.filter((r) => !read.includes(r));
19932
20544
  requests.length = 0;
19933
- requests.push(...left);
20545
+ appendAll(requests, left);
19934
20546
  console.error(`[wake] ${agentName}: dropped ${read.length} wake(s) for writes an answered request carried, and that it spoke after in the wake's channel where it carries one not marked ambient, or that were removed` +
19935
20547
  (requests.length === 0 ? '; no turn starts' : ''));
19936
20548
  if (requests.length === 0) {
@@ -20104,6 +20716,7 @@ export class AgentFramework {
20104
20716
  // channel is that message's, never one server's id with another's.
20105
20717
  channelId: channelReq?.channelId,
20106
20718
  channelServerId: channelReq?.channelServerId,
20719
+ wakeChannels: wakeChannelsOf(requests.some(bringsOwnChannels) ? requests.filter(hasOwnCauseForRun) : requests),
20107
20720
  // Only the mark this batch decided, never one requests[0] carried.
20108
20721
  autoContinueRun: continuationMark,
20109
20722
  addressed: addressedReq !== undefined,
@@ -20128,10 +20741,19 @@ export class AgentFramework {
20128
20741
  start[TURN_CARRY] = carry;
20129
20742
  else
20130
20743
  delete start[TURN_CARRY];
20744
+ const wait = requests.map(budgetWaitOf).find((w) => w !== undefined);
20745
+ if (wait)
20746
+ start[BUDGET_WAIT] = wait;
20747
+ else
20748
+ delete start[BUDGET_WAIT];
20131
20749
  if (requests.every(isRearmed))
20132
20750
  start[REARMED] = true;
20133
20751
  else
20134
20752
  delete start[REARMED];
20753
+ if (requests.every(isUnprompted))
20754
+ start[UNPROMPTED] = true;
20755
+ else
20756
+ delete start[UNPROMPTED];
20135
20757
  // A start merged from more than one request, any of them not the
20136
20758
  // turn's, records the batch it was merged from (MERGED_BATCH): until
20137
20759
  // it takes its token it stands for them all.
@@ -20160,7 +20782,7 @@ export class AgentFramework {
20160
20782
  */
20161
20783
  publishInputWithTurnLocus(input, agentName, callingTurn) {
20162
20784
  const given = input?.channelId;
20163
- if (typeof given === 'string')
20785
+ if (given !== undefined && given !== null)
20164
20786
  return input;
20165
20787
  // A locked turn's default is its lock, or none: never what the registry
20166
20788
  // resolves now, which reads the agent's latest routing (a silent start's).
@@ -20442,7 +21064,7 @@ export class AgentFramework {
20442
21064
  }
20443
21065
  if (held.length === 0)
20444
21066
  return held;
20445
- logical.drafts.push(...held);
21067
+ appendAll(logical.drafts, held);
20446
21068
  console.error(`[drafts] ${agent.name}: held ${held.length} segment(s) as ${held.map((d) => d.id).join(', ')} (${reason})`);
20447
21069
  this.emitTrace({
20448
21070
  type: 'prose:drafts-held',
@@ -21143,7 +21765,12 @@ export class AgentFramework {
21143
21765
  this.holdHeartbeatProse(agent, [text], round);
21144
21766
  return null;
21145
21767
  }
21146
- return this.channelRegistry.routeSpeech(agent.name, text, locus);
21768
+ return this.channelRegistry.routeSpeech(agent.name, text, locus, this.cameFrom(round.routing, locus));
21769
+ }
21770
+ cameFrom(routing, locus) {
21771
+ return locus === null && !routing.noChannel && routing.wakeChannels
21772
+ ? { cameFrom: routing.wakeChannels.map((c) => ({ channel: c, shown: this.channelShown(c) })) }
21773
+ : undefined;
21147
21774
  }
21148
21775
  /**
21149
21776
  * Hold a locked turn's plain speech as drafts ('no-destination'): the agent
@@ -21165,6 +21792,17 @@ export class AgentFramework {
21165
21792
  // A locked turn's locus is its lock, not a channel the heartbeat answered.
21166
21793
  return routing.heartbeat && (!routing.locus || routing.locked) ? { heartbeatTurn: true } : {};
21167
21794
  }
21795
+ unpromptedTurnMark(routing) {
21796
+ return routing.unprompted && !routing.locus ? { [UNPROMPTED]: true } : {};
21797
+ }
21798
+ wakeChannelsMark(routing) {
21799
+ return routing && !routing.noChannel && routing.wakeChannels ? { wakeChannels: routing.wakeChannels } : {};
21800
+ }
21801
+ turnRetryOf(trigger, routing) {
21802
+ const retry = asTurnRetry(trigger);
21803
+ delete retry.wakeChannels;
21804
+ return Object.assign(retry, this.wakeChannelsMark(routing));
21805
+ }
21168
21806
  /** Hold a heartbeat turn's plain speech as drafts ('no-destination'), named by `round`'s notice. */
21169
21807
  holdHeartbeatProse(agent, texts, round) {
21170
21808
  console.error(`[prose] ${agent.name}: heartbeat turn — ${texts.length} plain-speech segment(s) held as drafts (no destination)`);
@@ -21805,7 +22443,7 @@ export class AgentFramework {
21805
22443
  * and no undo or redo is made.
21806
22444
  */
21807
22445
  logicalTurnAlive(agent) {
21808
- return this.hasTurn(agent.name) || this.turnAlive(agent);
22446
+ return this.hasTurn(agent.name) || this.turnAlive(agent) || this.heldContinuation(agent.name);
21809
22447
  }
21810
22448
  /** Whether `agent` has a turn alive: starting, streaming or waiting on its tools. */
21811
22449
  turnAlive(agent) {
@@ -22548,7 +23186,7 @@ export class AgentFramework {
22548
23186
  }
22549
23187
  agentSees(agent, metadata) {
22550
23188
  const sees = this.storeSees.get(agent.getContextManager());
22551
- return !sees || sees(metadata);
23189
+ return !isReasoningLoss(metadata) && (!sees || sees(metadata));
22552
23190
  }
22553
23191
  /**
22554
23192
  * Record `view`, the filter `contextManager` was opened with
@@ -23169,6 +23807,8 @@ export class AgentFramework {
23169
23807
  source: 'framework',
23170
23808
  timestamp: Date.now(),
23171
23809
  ...this.heartbeatTurnMark(routing),
23810
+ ...this.unpromptedTurnMark(routing),
23811
+ ...this.wakeChannelsMark(routing),
23172
23812
  });
23173
23813
  }
23174
23814
  /**
@@ -23260,6 +23900,8 @@ export class AgentFramework {
23260
23900
  ...channel,
23261
23901
  autoContinueRun: { stop, ...(opts.heldAfterSend ? { heldAfterSend: true } : {}) },
23262
23902
  ...this.heartbeatTurnMark(routing),
23903
+ ...this.unpromptedTurnMark(routing),
23904
+ ...this.wakeChannelsMark(routing),
23263
23905
  });
23264
23906
  }
23265
23907
  /** How an autoContinue agent stops a run, as its `[continue]` row says it,
@@ -23686,7 +24328,7 @@ export class AgentFramework {
23686
24328
  }
23687
24329
  const target = sticky ?? locus;
23688
24330
  try {
23689
- const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target);
24331
+ const outcome = await this.channelRegistry.routeSpeech(agent.name, envelope, target, this.cameFrom(routing, target));
23690
24332
  this.recordProseDelivery(routing, outcome);
23691
24333
  if (outcome?.delivered) {
23692
24334
  routing.liveSpeech = true;
@@ -24041,6 +24683,8 @@ export class AgentFramework {
24041
24683
  source: 'framework',
24042
24684
  timestamp: Date.now(),
24043
24685
  ...this.heartbeatTurnMark(round.routing),
24686
+ ...this.unpromptedTurnMark(round.routing),
24687
+ ...this.wakeChannelsMark(round.routing),
24044
24688
  });
24045
24689
  }
24046
24690
  }
@@ -24174,7 +24818,7 @@ export class AgentFramework {
24174
24818
  const batch = trigger && mergedBatch(trigger);
24175
24819
  if (this.quiesced && batch) {
24176
24820
  this.releasePrimaryProviderGate(agent.name);
24177
- this.pendingRequests.push(...batch);
24821
+ appendAll(this.pendingRequests, batch);
24178
24822
  console.error(`[provider-admission] ${agent.name}: host quiesced while parked — the merged start goes back as its ${batch.length} requests`);
24179
24823
  return;
24180
24824
  }
@@ -24273,8 +24917,9 @@ export class AgentFramework {
24273
24917
  // one's routing, and an ephemeral run fails with the error rather than
24274
24918
  // waiting out its watchdog. A silent start has no part in an open turn.
24275
24919
  if (!trigger?.suppressProse) {
24920
+ const err = error instanceof Error ? error : new Error(String(error));
24276
24921
  this.settleAgent(agent.name, {
24277
- stopReason: 'exhausted', speech: '', error: error instanceof Error ? error.message : String(error),
24922
+ stopReason: 'exhausted', speech: '', error: failureWords(err), cause: err,
24278
24923
  }, this.openLogical.get(agent.name));
24279
24924
  }
24280
24925
  this.endStatedWaitRun(agent.name);
@@ -24301,7 +24946,7 @@ export class AgentFramework {
24301
24946
  this.activeTurnTokens.delete(agent.name);
24302
24947
  this.activeTurnTriggers.delete(agent.name);
24303
24948
  }
24304
- if (!tokenHandedOff && ownsProviderGate)
24949
+ if (!tokenHandedOff && ownsProviderGate && !checkWindow.gateReleased)
24305
24950
  this.releasePrimaryProviderGate(agent.name);
24306
24951
  // A start that ends before its stream exists is its turn's end: what
24307
24952
  // arrived during it enters its window, as at a stream's teardown,
@@ -24336,7 +24981,7 @@ export class AgentFramework {
24336
24981
  else {
24337
24982
  dropped = true;
24338
24983
  if (putsBack)
24339
- back.push(...mergedWakes(r));
24984
+ appendAll(back, mergedWakes(r));
24340
24985
  }
24341
24986
  }
24342
24987
  return left;
@@ -24356,7 +25001,7 @@ export class AgentFramework {
24356
25001
  this.releasePrimaryProviderGate(agentName);
24357
25002
  dropped = true;
24358
25003
  if (putsBack)
24359
- back.push(...mergedWakes(parked));
25004
+ appendAll(back, mergedWakes(parked));
24360
25005
  }
24361
25006
  for (const pass of this.passRequests) {
24362
25007
  const requests = pass.get(agentName);
@@ -24364,9 +25009,9 @@ export class AgentFramework {
24364
25009
  continue;
24365
25010
  const left = take(requests);
24366
25011
  requests.length = 0;
24367
- requests.push(...left);
25012
+ appendAll(requests, left);
24368
25013
  }
24369
- this.pendingRequests.push(...back);
25014
+ appendAll(this.pendingRequests, back);
24370
25015
  if (!dropped || !drop)
24371
25016
  return;
24372
25017
  // A primary a provider hold left pending for what it released won't run
@@ -24388,17 +25033,18 @@ export class AgentFramework {
24388
25033
  * ends on its own and settles nothing. What was stored for the agent
24389
25034
  * meanwhile is presented now (presentAtEndOf), or at that frame's end.
24390
25035
  */
24391
- endContinuedTurn(agentName, drop) {
24392
- console.error(`[turn-continuation] ${agentName}: its turn ends without its continuation: ${drop.why}`);
25036
+ endContinuedTurn(agentName, drop, opensTurn = false) {
25037
+ const ends = opensTurn ? 'its waiting start ends' : 'its turn ends without its continuation';
25038
+ console.error(`[turn-continuation] ${agentName}: ${ends}: ${drop.why}`);
24393
25039
  const agent = this.agents.get(agentName);
24394
25040
  const startedAt = this.lastInferenceAt.get(agentName)?.startedAt ?? Date.now();
24395
25041
  if (agent && drop.kind !== 'dropped') {
24396
25042
  const routing = this.openLogical.get(agentName) ?? this.routingOf(agentName);
24397
- this.endStoppedStart(agent, drop.kind === 'caller' ? { kind: 'caller', reason: drop.reason } : { kind: drop.kind }, routing, startedAt, undefined, false);
25043
+ this.endStoppedStart(agent, drop.kind === 'caller' ? { kind: 'caller', reason: drop.reason } : { kind: drop.kind }, routing, startedAt, undefined, opensTurn);
24398
25044
  }
24399
25045
  else {
24400
25046
  const owner = this.activeTurnTokens.get(agentName);
24401
- this.abortDroppedTurn(agentName, owner ?? -1, drop.kind === 'dropped' ? CONTINUATION_DROPPED : drop.kind === 'caller' ? (drop.reason ?? 'user') : drop.kind, startedAt);
25047
+ this.abortDroppedTurn(agentName, owner ?? -1, drop.kind === 'dropped' ? (drop.reason ?? CONTINUATION_DROPPED) : drop.kind === 'caller' ? (drop.reason ?? 'user') : drop.kind, startedAt);
24402
25048
  this.channelRegistry?.stopTyping();
24403
25049
  if (agent) {
24404
25050
  agent.toolResultGuard.recovering = false;
@@ -24418,7 +25064,7 @@ export class AgentFramework {
24418
25064
  // A registered ephemeral run's caller waits on the run's settle, which
24419
25065
  // no frame gives once its continuation is gone: the turn's end is the
24420
25066
  // run's, with the drop's reason.
24421
- this.settleAgent(agentName, { stopReason: 'exhausted', speech: '', error: `${agentName}'s turn ended without its continuation: ${drop.why}` }, undefined);
25067
+ this.settleAgent(agentName, { stopReason: 'exhausted', speech: '', error: `${agentName}'s ${opensTurn ? 'waiting start ended' : 'turn ended without its continuation'}: ${drop.why}` }, undefined);
24422
25068
  }
24423
25069
  // The turn's end; a frame that still holds its token presents at its
24424
25070
  // own end instead.
@@ -24639,6 +25285,8 @@ export class AgentFramework {
24639
25285
  /** This start's window (checkAhead), which its checks read for an
24640
25286
  * operator's abandon. */
24641
25287
  checkWindow) {
25288
+ if (attempt === 0 && this.budgetWaitEnded(agent, trigger))
25289
+ return false;
24642
25290
  const resumesTurn = resumesLogicalTurn(trigger?.reason);
24643
25291
  let carry = turnCarryOf(trigger);
24644
25292
  const turnProseRouting = trigger?.suppressProse ? 'disabled' : this.proseRoutingOf(agent);
@@ -24801,8 +25449,10 @@ export class AgentFramework {
24801
25449
  trigger = own;
24802
25450
  }
24803
25451
  // Record turn checkpoint before inference (only on first attempt, not retries)
25452
+ let checkpoint;
25453
+ let compileBaseline;
24804
25454
  if (attempt === 0 && trigger?.reason !== 'tool_result_guard_retry') {
24805
- this.recordTurnCheckpoint(agent.name);
25455
+ checkpoint = this.recordTurnCheckpoint(agent.name);
24806
25456
  this.redoStacks.delete(agent.name); // new work invalidates redo
24807
25457
  }
24808
25458
  // This start's routing: its logical turn's (TurnRouting), which holds
@@ -24845,6 +25495,10 @@ export class AgentFramework {
24845
25495
  routing.roundsRan = true;
24846
25496
  if (continued?.unflushedRounds)
24847
25497
  routing.unflushedRounds = continued.unflushedRounds;
25498
+ if (continued?.start)
25499
+ routing.start = continued.start;
25500
+ if (continued?.firstRow !== undefined)
25501
+ routing.firstRow = continued.firstRow;
24848
25502
  opensTurn = true;
24849
25503
  }
24850
25504
  else {
@@ -24860,8 +25514,11 @@ export class AgentFramework {
24860
25514
  }
24861
25515
  if (opensTurn) {
24862
25516
  const head = agent.branchHead();
24863
- if (head)
25517
+ if (head && !routing.start)
24864
25518
  routing.start = head;
25519
+ const firstRow = this.activeTurnTriggers.get(agent.name)?.readsRow;
25520
+ if (firstRow !== undefined)
25521
+ routing.firstRow = firstRow;
24865
25522
  // A silent start, and an agent whose configuration turns the pin off
24866
25523
  // (turnInputsPin: false), pin nothing, and a pin of the name left from
24867
25524
  // before goes.
@@ -24903,12 +25560,16 @@ export class AgentFramework {
24903
25560
  // a running turn without ending it, so the notice that writes the history
24904
25561
  // no longer holds are still on disk goes in before this start's compile.
24905
25562
  this.noticeHeadMoves(agent);
25563
+ const wakeChannels = wakeChannelsOf([routing, trigger]);
25564
+ if (wakeChannels)
25565
+ routing.wakeChannels = wakeChannels;
24906
25566
  if (opensTurn) {
24907
25567
  // The turn's trigger channel (item-3 redux): the channel of a
24908
25568
  // channel/DM-triggered turn, with its server, and none for a
24909
25569
  // no-trigger turn, which doesn't inherit a previous turn's channel: a
24910
25570
  // heartbeat's takes no locus at all (TurnRouting.heartbeat), and any
24911
- // other falls back to the global default. A wake whose channel,
25571
+ // other falls back to the global default, with the configured
25572
+ // unprompted channel in it where the turn takes it. A wake whose channel,
24912
25573
  // given without its server, names no one registered channel (a host's
24913
25574
  // id that two servers' channels answer to, or that none does) has none
24914
25575
  // either, and takes no fallback: it was for a channel, and the latest
@@ -24931,9 +25592,14 @@ export class AgentFramework {
24931
25592
  routing.heartbeat = true;
24932
25593
  else
24933
25594
  delete routing.heartbeat;
25595
+ if (isUnprompted(trigger))
25596
+ routing.unprompted = true;
25597
+ else
25598
+ delete routing.unprompted;
24934
25599
  // Resolve the turn's outbound locus now: the home channel for forks,
24935
25600
  // else the trigger channel, else the global default, which an
24936
- // ephemeral run never takes (takesGlobalFallback). Ambient mid-turn
25601
+ // ephemeral run never takes (takesGlobalFallback), with the configured
25602
+ // unprompted channel in it where the turn takes it. Ambient mid-turn
24937
25603
  // injections must not redirect the agent's plain prose (ambient
24938
25604
  // chatter and reactions hijacked it before, 2026-07-21); only an
24939
25605
  // addressed message, a follow-up in a channel the turn engaged
@@ -25019,7 +25685,9 @@ export class AgentFramework {
25019
25685
  ? 'at your budget restart'
25020
25686
  : isTurnContinuation(trigger?.reason ?? '')
25021
25687
  ? 'before this re-inference on your tool results'
25022
- : 'while your turn waited on a provider hold';
25688
+ : budgetWaitOf(trigger)
25689
+ ? 'while your turn waited'
25690
+ : 'while your turn waited on a provider hold';
25023
25691
  this.noticeAddressedArrivals(agent, routing, arrivals, when);
25024
25692
  }
25025
25693
  }
@@ -25165,13 +25833,16 @@ export class AgentFramework {
25165
25833
  if (carry && this.heardSince(agent, routing, carry.read))
25166
25834
  carry = undefined;
25167
25835
  const streamCarry = { silence: carry?.silence ?? null, sends: carry?.sends ?? null, read: lastHeardAt };
25168
- const { stream, request: compiledRequest, takeKvSubmission, drainKvSubmissionIds, continuePrompt, placeDrops, thinkingRecordedAfter, thinkingBranch, noteRequestSent, carriedBodies, } = await agent.startStreamWithInjections(tools, injections, undefined, compressionTools, {
25836
+ compileBaseline = { progress: this.compressionProgressOf(agent), settings: agent.contextSettingsChanges() };
25837
+ const { stream, request: compiledRequest, takeKvSubmission, drainKvSubmissionIds, continuePrompt, placeDrops, thinkingRecordedAfter, thinkingBranch, noteRequestSent, noteThinkingDropped, carriedBodies, } = await agent.startStreamWithInjections(tools, injections, undefined, compressionTools, {
25169
25838
  silentHeartbeat: trigger?.silentHeartbeat,
25170
25839
  // This turn's retries are the framework's (errorPolicy below), not
25171
25840
  // membrane's inside each attempt.
25172
25841
  membraneRetries: false,
25173
25842
  inferenceBounds: this.inferenceBounds,
25174
25843
  });
25844
+ if (trigger)
25845
+ delete trigger[BUDGET_WAIT];
25175
25846
  // Each later round this stream sends is noted as it goes (noteRoundSent).
25176
25847
  if (noteRequestSent)
25177
25848
  this.streamRequestNotes.set(stream, { note: noteRequestSent, rounds: [], stopped: false });
@@ -25226,7 +25897,7 @@ export class AgentFramework {
25226
25897
  if (this.registry.homeChannel(agent.name) !== undefined) {
25227
25898
  const wait = this.providerWaits?.active(agent.name, agent.model);
25228
25899
  // The turn has begun (inference:started), so the hold keeps its retry.
25229
- if (wait && this.holdForProviderWait(agent, wait, trigger ? [asTurnRetry(withTurnCarry(trigger, carry))] : [], false, () => {
25900
+ if (wait && this.holdForProviderWait(agent, wait, trigger ? [this.turnRetryOf(withTurnCarry(trigger, carry), routing)] : [], false, () => {
25230
25901
  // The stream the compile made won't be sent: it's cancelled, and
25231
25902
  // this start's window closes, before the hold is reported, so a stop
25232
25903
  // made while inference:failed is delivered finds neither, and the
@@ -25279,6 +25950,7 @@ export class AgentFramework {
25279
25950
  place: placeDrops,
25280
25951
  ...(thinkingRecordedAfter !== undefined ? { recordedAfter: thinkingRecordedAfter } : {}),
25281
25952
  ...(thinkingBranch !== undefined ? { branch: thinkingBranch } : {}),
25953
+ ...(noteThinkingDropped ? { noteDropped: noteThinkingDropped } : {}),
25282
25954
  }
25283
25955
  : undefined, streamCarry);
25284
25956
  this.activeStreams.set(agent.name, handle);
@@ -25326,6 +25998,19 @@ export class AgentFramework {
25326
25998
  }
25327
25999
  const retry = trigger && withTurnCarry(trigger, carry);
25328
26000
  const asTransient = this.takeAsTransient(agent.name, err, trigger);
26001
+ if (this.holdForBudgetRefusal(agent, err, retry, checkpoint, routing, compileBaseline, () => {
26002
+ if (this.checkAhead.get(agent) === checkWindow)
26003
+ this.checkAhead.delete(agent);
26004
+ if (this.activeTurnTokens.get(agent.name) === turnToken) {
26005
+ this.activeTurnTokens.delete(agent.name);
26006
+ this.activeTurnTriggers.delete(agent.name);
26007
+ }
26008
+ if (ownsProviderGate && !checkWindow.gateReleased) {
26009
+ checkWindow.gateReleased = true;
26010
+ this.releasePrimaryProviderGate(agent.name);
26011
+ }
26012
+ }))
26013
+ return false;
25329
26014
  if (ownsProviderGate && this.holdProviderAcceleration(agent, err, retry)) {
25330
26015
  // Capacity scheduling is not poisoned history. The cooldown timer owns
25331
26016
  // one later fresh compile; no immediate retry or exhaustion marker.
@@ -25388,7 +26073,7 @@ export class AgentFramework {
25388
26073
  // drop), not a window nothing will collect.
25389
26074
  if (this.checkAhead.get(agent) === checkWindow)
25390
26075
  this.checkAhead.delete(agent);
25391
- await this.startAgentStream(agent, retry && asTurnRetry(retry), attempt + 1);
26076
+ await this.startAgentStream(agent, retry && this.turnRetryOf(retry, routing), attempt + 1);
25392
26077
  }
25393
26078
  else {
25394
26079
  this.noteFailedTurnEnd(agent, routing);
@@ -25406,7 +26091,8 @@ export class AgentFramework {
25406
26091
  this.settleAgent(agent.name, {
25407
26092
  stopReason: 'exhausted',
25408
26093
  speech: '',
25409
- ...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
26094
+ ...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: failureWords(err) }),
26095
+ cause: err,
25410
26096
  }, routing);
25411
26097
  if (isRearmed(trigger) && asTransient)
25412
26098
  this.exhaustingRearms.add(agent.name);
@@ -25414,6 +26100,7 @@ export class AgentFramework {
25414
26100
  type: 'inference:exhausted',
25415
26101
  agentName: agent.name,
25416
26102
  error: err.message,
26103
+ ...shownReasonOf(err),
25417
26104
  // Drives the poison-history breaker (shedMayHelp: an
25418
26105
  // `invalid_request` at HTTP 400, 413 or 422 or with no status, or a
25419
26106
  // `context_length` at 413, suggests the provider refused the
@@ -26376,7 +27063,7 @@ export class AgentFramework {
26376
27063
  this.noteRefusal(agent.name, category, tokenUsage);
26377
27064
  countUsage(abandoned);
26378
27065
  logStream({
26379
- success: false, error: 'Tool output withheld by the guard',
27066
+ success: false, error: 'Tool output withheld by the guard', errorWords: 'Tool output withheld by the guard',
26380
27067
  request: compiledRequest ?? {}, response, durationMs, tokenUsage, stopReason: 'refusal',
26381
27068
  });
26382
27069
  console.error(`[tool-result-guard] agent=${agent.name} withheld ${withheld.length} result(s); retrying inference`);
@@ -26436,10 +27123,12 @@ export class AgentFramework {
26436
27123
  // a postscript to a message already delivered.
26437
27124
  // Restart inference inside this logical turn. No tool is
26438
27125
  // executed again and no settle/checkpoint/locus reset occurs.
26439
- await this.startAgentStream(agent, withTurnCarry({
27126
+ const retried = {
26440
27127
  ...trigger, agentName: agent.name, reason: 'tool_result_guard_retry',
26441
27128
  source: trigger?.source ?? 'framework', timestamp: Date.now(),
26442
- }, { silence, sends: turnSends, read: carry.read }), attempt);
27129
+ };
27130
+ delete retried.wakeChannels;
27131
+ await this.startAgentStream(agent, withTurnCarry({ ...retried, ...this.wakeChannelsMark(streamRouting) }, { silence, sends: turnSends, read: carry.read }), attempt);
26443
27132
  return;
26444
27133
  }
26445
27134
  const lastResult = response.content.reduce((last, block, index) => block.type === 'tool_result' ? index : last, -1);
@@ -26821,6 +27510,7 @@ export class AgentFramework {
26821
27510
  reason: forced ? 'unstick-retry' : 'refusal-rewind-retry',
26822
27511
  source: 'framework',
26823
27512
  timestamp: Date.now(),
27513
+ ...this.wakeChannelsMark(streamRouting),
26824
27514
  });
26825
27515
  handledByRewind = true;
26826
27516
  };
@@ -27193,7 +27883,7 @@ export class AgentFramework {
27193
27883
  });
27194
27884
  logStream({
27195
27885
  success: false,
27196
- error: err.message,
27886
+ ...failedEntry('', err),
27197
27887
  request: compiledRequest ?? { note: 'streaming request failed' },
27198
27888
  durationMs,
27199
27889
  });
@@ -27261,7 +27951,7 @@ export class AgentFramework {
27261
27951
  return;
27262
27952
  }
27263
27953
  // Marked as its turn's retry, as at the start's failure.
27264
- await this.startAgentStream(agent, retry && asTurnRetry(retry), attempt + 1);
27954
+ await this.startAgentStream(agent, retry && this.turnRetryOf(retry, streamRouting), attempt + 1);
27265
27955
  }
27266
27956
  else {
27267
27957
  // The turn ends here, and its inference:failed doesn't say so,
@@ -27286,7 +27976,8 @@ export class AgentFramework {
27286
27976
  this.settleAgent(agent.name, {
27287
27977
  stopReason: 'exhausted',
27288
27978
  speech: '',
27289
- ...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
27979
+ ...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: failureWords(err) }),
27980
+ cause: err,
27290
27981
  }, this.agents.get(agent.name) === agent && agent.streamId === myStreamId ? streamRouting : undefined, streamRouting);
27291
27982
  if (isRearmed(trigger) && asTransient)
27292
27983
  this.exhaustingRearms.add(agent.name);
@@ -27294,6 +27985,7 @@ export class AgentFramework {
27294
27985
  type: 'inference:exhausted',
27295
27986
  agentName: agent.name,
27296
27987
  error: err.message,
27988
+ ...shownReasonOf(err),
27297
27989
  // Drives the poison-history breaker. `retryable` is kept for
27298
27990
  // observability, but the breaker gates on
27299
27991
  // shedMayHelp(errorType, httpStatus): only an `invalid_request`
@@ -27382,6 +28074,7 @@ export class AgentFramework {
27382
28074
  type: 'inference:exhausted',
27383
28075
  agentName: agent.name,
27384
28076
  error: terminal,
28077
+ shownReason: terminal,
27385
28078
  });
27386
28079
  this.rearmExhaustedTurn(agent, new Error(terminal), trigger, ownsProviderGate, streamRouting, false);
27387
28080
  // Postmortem 2026-05-28 P2 #7: persist the abort to the
@@ -27391,6 +28084,7 @@ export class AgentFramework {
27391
28084
  logStream({
27392
28085
  success: false,
27393
28086
  error: terminal,
28087
+ errorWords: terminal,
27394
28088
  request: compiledRequest ?? { note: 'streaming request aborted' },
27395
28089
  durationMs,
27396
28090
  });
@@ -27475,10 +28169,13 @@ export class AgentFramework {
27475
28169
  // carries what the stream appended, so once a call has dropped
27476
28170
  // blocks, none of the stream's later calls is given either. A
27477
28171
  // compile whose first call dropped blocks gives no sample, and the
27478
- // calibration keeps its value. If the provider drops an earlier
27479
- // model's blocks after a model switch (`model_binding_mismatch`),
27480
- // they drop on every call until they leave the context, and the
27481
- // calibration takes up the new model's tokenizer only after that.
28172
+ // calibration keeps its value. Without `binds`, if the provider
28173
+ // drops an earlier model's blocks after a model switch
28174
+ // (`model_binding_mismatch`), they drop on every call until they
28175
+ // leave the context, and the calibration takes up the new model's
28176
+ // tokenizer only after that. With `binds`, the switch changes the
28177
+ // request's identity, so the compile leaves the replies' thinking
28178
+ // out itself, and a drop that does come is noted below.
27482
28179
  const thinkingReport = event.round?.thinking;
27483
28180
  const thinkingDropped = (thinkingReport?.dropped.length ?? 0) > 0;
27484
28181
  if (thinkingDropped)
@@ -27486,6 +28183,18 @@ export class AgentFramework {
27486
28183
  // Against the branch's binding record, when the context manager keeps one: a drop it can't account
27487
28184
  // for tells the operators (noteThinkingLoss), and a call with no report shows no such drop.
27488
28185
  const placedDrops = thinkingDrops && thinkingReport ? thinkingDrops.place(thinkingReport.dropped) : [];
28186
+ // The stored replies' bodies among them, as placed, go to the record, so the next compile sends them
28187
+ // without their thinking and the provider drops none of them again.
28188
+ const droppedBodies = placedDrops.flatMap((drop) => drop.kind === 'reply' ? drop.bodies.map((body) => body.messageId) : []);
28189
+ if (droppedBodies.length > 0 && thinkingDrops?.noteDropped) {
28190
+ try {
28191
+ thinkingDrops.noteDropped(droppedBodies);
28192
+ }
28193
+ catch (error) {
28194
+ console.error(`[thinking-binding] ${agent.name}: couldn't note the stored replies whose thinking the provider dropped, ` +
28195
+ 'so the next compile still sends them with it:', error);
28196
+ }
28197
+ }
27489
28198
  if (thinkingDrops?.recordedAfter !== undefined) {
27490
28199
  this.noteThinkingLoss(agent.name, placedDrops, thinkingDrops.recordedAfter);
27491
28200
  }
@@ -27628,7 +28337,7 @@ export class AgentFramework {
27628
28337
  // ends here (endFailureAtStop).
27629
28338
  this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime, streamRouting);
27630
28339
  logStream({ success: false,
27631
- error: `Provider acceleration cooldown: ${err.message}`,
28340
+ ...failedEntry('Provider acceleration cooldown: ', err),
27632
28341
  request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
27633
28342
  this.abandonAgentScripts(agent.name, 'provider acceleration cooldown', streamRouting);
27634
28343
  lifecyclePhase = 'failed';
@@ -27645,7 +28354,7 @@ export class AgentFramework {
27645
28354
  if (statedWait === 'held') {
27646
28355
  this.endFailureAtStop(agent.name, myTurnToken ?? -1, startTime, streamRouting);
27647
28356
  logStream({ success: false,
27648
- error: `Provider wait: ${err.message}`,
28357
+ ...failedEntry('Provider wait: ', err),
27649
28358
  request: compiledRequest ?? { note: 'streaming request rate-limited' }, durationMs });
27650
28359
  this.abandonAgentScripts(agent.name, 'provider wait', streamRouting);
27651
28360
  lifecyclePhase = 'failed';
@@ -27658,7 +28367,8 @@ export class AgentFramework {
27658
28367
  this.settleAgent(agent.name, {
27659
28368
  stopReason: 'exhausted',
27660
28369
  speech: '',
27661
- ...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: err.message }),
28370
+ ...(statedWait === 'fail' ? this.statedWaitNotSlept(err) : { error: failureWords(err) }),
28371
+ cause: err,
27662
28372
  }, streamRouting);
27663
28373
  if (isRearmed(trigger) && asTransient)
27664
28374
  this.exhaustingRearms.add(agent.name);
@@ -27666,6 +28376,7 @@ export class AgentFramework {
27666
28376
  type: 'inference:exhausted',
27667
28377
  agentName: agent.name,
27668
28378
  error: err.message,
28379
+ ...shownReasonOf(err),
27669
28380
  ...this.classifyInferenceError(err),
27670
28381
  });
27671
28382
  this.rearmExhaustedTurn(agent, err, trigger, ownsProviderGate, streamRouting, asTransient);
@@ -27674,7 +28385,7 @@ export class AgentFramework {
27674
28385
  // Persist to inference log for forensic attribution.
27675
28386
  logStream({
27676
28387
  success: false,
27677
- error: `Stream threw: ${err.message}`,
28388
+ ...failedEntry('Stream threw: ', err),
27678
28389
  request: compiledRequest ?? { note: 'streaming request threw' },
27679
28390
  durationMs,
27680
28391
  });
@@ -27700,13 +28411,15 @@ export class AgentFramework {
27700
28411
  if (countedUsage && !usageLogged) {
27701
28412
  const replaced = this.agents.get(agent.name) !== agent;
27702
28413
  const superseded = generationLost || agent.streamId !== myStreamId;
28414
+ const ended = replaced
28415
+ ? 'Stream abandoned: its agent was disposed or replaced'
28416
+ : superseded
28417
+ ? 'Stream superseded: a newer stream of the agent took over'
28418
+ : 'Stream ended without a final response';
27703
28419
  logStream({
27704
28420
  success: false,
27705
- error: replaced
27706
- ? 'Stream abandoned: its agent was disposed or replaced'
27707
- : superseded
27708
- ? 'Stream superseded: a newer stream of the agent took over'
27709
- : 'Stream ended without a final response',
28421
+ error: ended,
28422
+ errorWords: ended,
27710
28423
  request: compiledRequest ?? { note: 'streaming request' },
27711
28424
  durationMs: Date.now() - startTime,
27712
28425
  tokenUsage: tokenUsageOf(countedUsage),
@@ -27857,7 +28570,7 @@ export class AgentFramework {
27857
28570
  // so does one whose failure a provider hold keeps for its retry: the
27858
28571
  // turn isn't over (pendingContinuationAgents).
27859
28572
  if (frameReachedTerminal && ownsPhysicalStream && this.closingConversationAgents.has(agent.name)
27860
- && !this.pendingContinuationAgents().has(agent.name)) {
28573
+ && !this.pendingContinuationAgents().has(agent.name) && !this.closureWaits(agent.name)) {
27861
28574
  this.disposeConversationAgent(agent.name, 'idle-ttl');
27862
28575
  }
27863
28576
  }
@@ -27950,7 +28663,7 @@ export class AgentFramework {
27950
28663
  if (this.surgeryHold) {
27951
28664
  throw new Error(`puppet refused: the store is under live ${this.surgeryHold.verb} for ${this.surgeryHold.agentName} — retry when it completes`);
27952
28665
  }
27953
- if (agent.state.status !== 'idle' || this.hasTurn(agentName)) {
28666
+ if (agent.state.status !== 'idle' || this.hasTurn(agentName) || this.heldContinuation(agentName)) {
27954
28667
  const shown = agent.state.status === 'idle' ? 'idle+turn-alive' : agent.state.status;
27955
28668
  // A turn a provider hold keeps for its retry has no stream: the refusal
27956
28669
  // names the hold, and how to go on sooner, as surgery's does.
@@ -27958,8 +28671,11 @@ export class AgentFramework {
27958
28671
  throw new Error(held
27959
28672
  ? `puppet refused: agent ${agentName} is idle+turn-alive (${held.says}; requires idle); ` +
27960
28673
  heldTurnWayOn(held.statedWait)
27961
- : `puppet refused: agent ${agentName} is ${shown} (requires idle — ` +
27962
- `injecting a turn under an active stream corrupts wire ordering)`);
28674
+ : agent.state.status === 'idle' && !this.hasTurn(agentName)
28675
+ ? `puppet refused: agent ${agentName} is idle+turn-alive (its turn waits for compression to make room; ` +
28676
+ 'requires idle); abortInference() ends that turn'
28677
+ : `puppet refused: agent ${agentName} is ${shown} (requires idle — ` +
28678
+ `injecting a turn under an active stream corrupts wire ordering)`);
27963
28679
  }
27964
28680
  const onSurface = this.availableToolsForPresentation(agentName).some((t) => t.name === toolName);
27965
28681
  if (!onSurface) {
@@ -29742,6 +30458,8 @@ export class AgentFramework {
29742
30458
  // produced it is already stored separately as a blob.
29743
30459
  if (typeof entryToStore.error === 'string')
29744
30460
  entryToStore.error = boundFailureText(entryToStore.error);
30461
+ if (typeof entryToStore.errorWords === 'string')
30462
+ entryToStore.errorWords = boundFailureText(entryToStore.errorWords);
29745
30463
  // Blob threshold: 10KB - typical context-heavy requests exceed this
29746
30464
  const BLOB_THRESHOLD = 10000;
29747
30465
  // Both branches persist the JSON view, never the live object: a compiled
@@ -29959,20 +30677,22 @@ export class AgentFramework {
29959
30677
  }
29960
30678
  /**
29961
30679
  * Why this host refuses a skip_reply call, as the model will read it, or
29962
- * undefined when it doesn't (skipReplyWakeRefusal): without an EventGate,
29963
- * any wake_in_seconds; with one, a wake_in_seconds that isn't a number of
29964
- * seconds. Only when the channel registry handles skip_reply. The dispatch
29965
- * that refuses the call reads it here, and so does the turn's bookkeeping of
29966
- * a refused round (roundCallOf), for the model's calls and a script's calls
29967
- * that reach the host while a call of the round watches it: the silence
29968
- * before it, which comes back after its round, and in XML mode which round's
29969
- * prose stays private. A skip_reply round's own prose stays private by the
29970
- * tool's name, refused or not.
30680
+ * undefined when it doesn't: a reason that isn't a string
30681
+ * (ChannelRegistry.nonStringRefusal), or (skipReplyWakeRefusal) without an
30682
+ * EventGate, any wake_in_seconds, and with one, a wake_in_seconds that isn't
30683
+ * a number of seconds. Only when the channel registry handles skip_reply.
30684
+ * The dispatch that refuses the call reads it here, and so does the turn's
30685
+ * bookkeeping of a refused round (roundCallOf), for the model's calls and a
30686
+ * script's calls that reach the host while a call of the round watches it:
30687
+ * the silence before it, which comes back after its round, and in XML mode
30688
+ * which round's prose stays private. A skip_reply round's own prose stays
30689
+ * private by the tool's name, refused or not.
29971
30690
  */
29972
30691
  skipReplyRefusal(call) {
29973
30692
  if (call.name !== 'skip_reply' || !this.channelRegistry)
29974
30693
  return undefined;
29975
- return skipReplyWakeRefusal(readSkipReplyWake(call.input), !!this.eventGate);
30694
+ return this.channelRegistry.nonStringRefusal('skip_reply', call.input)?.error
30695
+ ?? skipReplyWakeRefusal(readSkipReplyWake(call.input), !!this.eventGate);
29976
30696
  }
29977
30697
  refusesSelfWake(call) {
29978
30698
  return this.skipReplyRefusal(call) !== undefined;
@@ -29981,8 +30701,8 @@ export class AgentFramework {
29981
30701
  * A call as its round's routing counts it, made directly, or by a script
29982
30702
  * and reaching the host while a run or wait call of the round watches it
29983
30703
  * (ScriptRound). A skip_reply refused, because the agent's allowedTools
29984
- * doesn't name it, because its arguments are refused (markedInputRefusal)
29985
- * or because this host can't set the wake it asks for, is
30704
+ * doesn't name it, because its arguments are refused (markedInputRefusal),
30705
+ * or because this host refuses its arguments (skipReplyRefusal), is
29986
30706
  * marked refused: it keeps its own round private and silences nothing after
29987
30707
  * it. A refusal withholds the call's effect, not the model's choice to keep
29988
30708
  * its words private. Any other call the list refuses posted nothing, so it
@@ -30239,7 +30959,7 @@ export class AgentFramework {
30239
30959
  const error = this.skipReplyRefusal(enrichedCall);
30240
30960
  if (error !== undefined) {
30241
30961
  const wakeValue = enrichedCall.input?.wake_in_seconds;
30242
- console.error(`[self-wake] agent=${agentName} wake_in_seconds=${logValue(wakeValue)} skip_reply refused: ${logDetail(this.eventGate ? "wake_in_seconds isn't a number of seconds" : 'No EventGate configured (FrameworkConfig.gate is unset)')}`);
30962
+ console.error(`[self-wake] agent=${agentName} wake_in_seconds=${logValue(wakeValue)} skip_reply refused: ${logDetail(error)}`);
30243
30963
  const result = this.refusedResult(agentName, enrichedCall.id, error);
30244
30964
  this.emitTrace({ type: 'tool:failed', module: 'channels', tool: enrichedCall.name, callId: enrichedCall.id, error });
30245
30965
  this.pushEvent({ type: 'tool-result', callId: enrichedCall.id, toolName: enrichedCall.name, agentName, moduleName: 'channels', result });
@@ -31803,6 +32523,7 @@ export class AgentFramework {
31803
32523
  this.noteInferenceExhausted(event.agentName ?? 'unknown', event.error ?? 'unknown error', event.retryable, event.errorType, {
31804
32524
  httpStatus: event.httpStatus,
31805
32525
  providerErrorCode: event.providerErrorCode,
32526
+ shownReason: event.shownReason,
31806
32527
  });
31807
32528
  }
31808
32529
  else if (event.type === 'inference:completed') {
@@ -31868,6 +32589,8 @@ export class AgentFramework {
31868
32589
  bounded.error = boundFailureText(bounded.error);
31869
32590
  if (typeof bounded.stack === 'string')
31870
32591
  bounded.stack = boundFailureText(bounded.stack);
32592
+ if (typeof bounded.shownReason === 'string')
32593
+ bounded.shownReason = boundFailureText(bounded.shownReason);
31871
32594
  if (this.earlyTraces) {
31872
32595
  if (this.earlyTraces.length < 1000)
31873
32596
  this.earlyTraces.push(traceEvent);
@@ -31967,6 +32690,20 @@ export class AgentFramework {
31967
32690
  };
31968
32691
  })(),
31969
32692
  lastInference: this.lastInferenceAt.get(name) ?? null,
32693
+ budgetHold: (() => {
32694
+ const hold = this.budgetHolds?.get(name);
32695
+ if (!hold)
32696
+ return null;
32697
+ const readiness = this.contextReadiness(agent.getContextManager());
32698
+ return {
32699
+ since: hold.startedAt,
32700
+ pending: 'pending' in readiness && readiness.pending ? boundFailureText(readiness.pending) : null,
32701
+ heldRequests: 1 + hold.wakes.flatMap((r) => mergedBatch(r) ?? [r]).filter((r) => !passesQuiesce(r)).length,
32702
+ errorType: hold.errorType,
32703
+ ...(hold.budget !== undefined ? { budget: hold.budget } : {}),
32704
+ ...(hold.actual !== undefined ? { actual: hold.actual } : {}),
32705
+ };
32706
+ })(),
31970
32707
  refusalStats: this.refusalStats.get(name) ?? null,
31971
32708
  ...this.upcomingWakesHealth(name),
31972
32709
  })),
@@ -31990,7 +32727,8 @@ export class AgentFramework {
31990
32727
  }
31991
32728
  /**
31992
32729
  * Ops alert — the one escalation path for "a human should hear about this":
31993
- * (1) durable failures.log record (unless the caller already wrote one),
32730
+ * (1) durable failures.log record (unless the caller already wrote one, or
32731
+ * the alert keeps none, as a budget hold's open and clear don't),
31994
32732
  * (2) an `ops:alert` trace so authorized observers get it on the wire,
31995
32733
  * (3) if CONNECTOME_OPS_WEBHOOK is set, a Discord post throttled to one per
31996
32734
  * (agent, kind) per cooldown window — a persistent failure re-posts
@@ -32024,7 +32762,7 @@ export class AgentFramework {
32024
32762
  notes.stopped = true;
32025
32763
  return;
32026
32764
  }
32027
- notes.rounds.push(...rows);
32765
+ appendAll(notes.rounds, rows);
32028
32766
  this.noteStreamRows(agent, notes);
32029
32767
  }
32030
32768
  /**
@@ -32233,17 +32971,8 @@ export class AgentFramework {
32233
32971
  ...(providerErrorCode !== undefined ? { providerErrorCode } : {}),
32234
32972
  };
32235
32973
  }
32236
- if (err instanceof OverBudgetError || err.name === 'OverBudgetError') {
32237
- return { errorType: 'over_budget' };
32238
- }
32239
- // context-manager's fatal coverage invariant (fix/coverage-invariant-fatal,
32240
- // 2026-07-26): a compile REFUSES rather than shipping a context with
32241
- // silently-dropped messages. Recovery is identical to over_budget — kick
32242
- // the compression drain so summaries cover the un-represented span.
32243
- if (err instanceof UncoveredDropError || err.name === 'UncoveredDropError') {
32244
- return { errorType: 'context_refusal' };
32245
- }
32246
- return {};
32974
+ const refusal = compileRefusalType(err);
32975
+ return refusal !== undefined ? { errorType: refusal } : {};
32247
32976
  }
32248
32977
  /**
32249
32978
  * Public entry to the ops-alert pipeline (failures.log + ops:alert trace +
@@ -32266,10 +32995,12 @@ export class AgentFramework {
32266
32995
  // sink below gets an excerpt cut from the whole reason at its own bound
32267
32996
  // (boundFailureText, so its marker states the whole length): the stderr
32268
32997
  // line, failures.log and the hard-down alert FAILURE_TEXT_MAX_CHARS, the
32269
- // [inference-failed] marker FAILURE_MARKER_REASON_MAX_CHARS, the status's
32270
- // lastError and the compile-refused alert FAILURE_SUMMARY_MAX_CHARS, and
32271
- // the rewind marker REWIND_MARKER_REASON_MAX_CHARS. The classification
32272
- // travels as structured fields beside the text.
32998
+ // [inference-failed] marker's metadata FAILURE_MARKER_REASON_MAX_CHARS, the
32999
+ // status's lastError and the compile-refused alert FAILURE_SUMMARY_MAX_CHARS,
33000
+ // and the rewind marker's metadata REWIND_MARKER_REASON_MAX_CHARS. The
33001
+ // classification travels as structured fields beside the text. The
33002
+ // [inference-failed] marker's text quotes only `shownReason`, which holds
33003
+ // no provider's text, and the rewind marker's quotes no reason.
32273
33004
  //
32274
33005
  // A reason that isn't a string (the stream paths always pass a message)
32275
33006
  // is described rather than allowed to throw before anything is recorded.
@@ -32311,9 +33042,12 @@ export class AgentFramework {
32311
33042
  const agent = this.agents.get(agentName);
32312
33043
  if (agent && !(rearmed && !ended) && process.env.SUPPRESS_INFERENCE_FAILED_MARKER !== '1') {
32313
33044
  const timedOut = errorType === 'timeout';
32314
- const causeParts = [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
33045
+ const causeParts = errorType === 'safety' ? [] : [errorType, httpStatus !== undefined ? `HTTP ${httpStatus}` : undefined]
32315
33046
  .filter((part) => part !== undefined);
32316
33047
  const cause = timedOut && causeParts.length === 1 ? '' : causeParts.join(', ');
33048
+ const shownText = typeof detail?.shownReason === 'string'
33049
+ ? failureShown(boundFailureText(detail.shownReason, FAILURE_MARKER_REASON_MAX_CHARS)) : undefined;
33050
+ const shown = shownText !== undefined ? ` Reason: ${shownText}${/[.!?]$/.test(shownText) ? '' : '.'}` : '';
32317
33051
  try {
32318
33052
  storeOwnRow(agent, 'user', [{
32319
33053
  type: 'text',
@@ -32324,11 +33058,11 @@ export class AgentFramework {
32324
33058
  `${ended.roundsRan ? ' Its earlier rounds ran, and their tool calls ran as their results show.' : ''}` +
32325
33059
  `${ended.callsCutOff ? ` ${CALLS_CUT_OFF}` : ''}` +
32326
33060
  `${ended.liveSpeech ? ` ${LIVE_SPEECH_UNRECEIPTED}` : ''}` +
32327
- `${ended.speechRouting ? ` ${SPEECH_STILL_ROUTING}` : ''} Reason: `
33061
+ `${ended.speechRouting ? ` ${SPEECH_STILL_ROUTING}` : ''}`
32328
33062
  : `[${NOTICE_TAG.inferenceFailed}] Your previous turn did not complete: ` +
32329
33063
  `the model call ${timedOut ? 'timed out' : 'failed'}${cause ? ` (${suppliedValue(cause)})` : ''} ` +
32330
- `and produced no response, so nothing was sent.${madeAgain ? ` ${MADE_AGAIN}` : ''} Reason: `) +
32331
- `${failureShown(markerReason)}. ${timedOut ? TIMEOUT_ADVICE : FAILURE_ADVICE}`,
33064
+ `and produced no response, so nothing was sent.${madeAgain ? ` ${MADE_AGAIN}` : ''}`) +
33065
+ `${shown} ${timedOut ? TIMEOUT_ADVICE : FAILURE_ADVICE}`,
32332
33066
  }], { system: true, kind: 'inference-failed', reason: markerReason, consecutive: streak, ...classification, ...cutAt(markerReason) });
32333
33067
  }
32334
33068
  catch (err) {
@@ -32369,49 +33103,7 @@ export class AgentFramework {
32369
33103
  // runUntilIdle() has no maintenance timer, so a drain is its recovery.
32370
33104
  const drainState = !overBudget ? undefined
32371
33105
  : !agent ? 'unavailable (no agent handle)'
32372
- : this.providerAdmissionClosed ? 'not kicked: the framework is stopping'
32373
- : this.surgeryHold ? 'not kicked: a surgery holds the store'
32374
- : this.overBudgetDrainInFlight.has(agentName) ? 'already draining'
32375
- : 'kicking';
32376
- if (agent && drainState === 'kicking') {
32377
- this.overBudgetDrainInFlight.add(agentName);
32378
- // Like a maintenance pass: none starts once stop() has begun, it starts
32379
- // no tick while a surgery holds the store or once its agent has been
32380
- // disposed, and a surgery, a switchBranch or stop() waits for the tick
32381
- // in flight. It joins the drains they wait for before the alert is
32382
- // raised and before its body runs, so the alert's listeners find it:
32383
- // one that begins stop() or a surgery waits for it, and its body then
32384
- // ticks none and says why. So does a stop() made from inside its first
32385
- // tick, before that tick's first await.
32386
- const drain = Promise.resolve().then(async () => {
32387
- let ticks = 0;
32388
- try {
32389
- const cm = agent.getContextManager();
32390
- while (ticks < 8 && !this.providerAdmissionClosed && !this.surgeryHold && this.agents.get(agentName) === agent) {
32391
- await cm.tick();
32392
- ticks++;
32393
- }
32394
- const stopped = ticks >= 8 ? ''
32395
- : this.agents.get(agentName) !== agent ? ', stopped as the agent was disposed'
32396
- : this.providerAdmissionClosed ? ', stopped as the framework stops'
32397
- : ', stopped while a surgery holds the store';
32398
- console.error(`[inference-failed] drain kicked for ${agentName} (OverBudget breaker, ${ticks} ticks${stopped})`);
32399
- }
32400
- catch (err) {
32401
- if (err instanceof MaintenanceWithdrawnError) {
32402
- console.error(`[inference-failed] drain kicked for ${agentName} (OverBudget breaker, ${ticks} ticks, stopped: ${err.message})`);
32403
- }
32404
- else {
32405
- console.error(`[inference-failed] drain kick failed for ${agentName} after ${ticks} ticks: ${describeFailureWithFrames(err)}`);
32406
- }
32407
- }
32408
- finally {
32409
- this.overBudgetDrainInFlight.delete(agentName);
32410
- }
32411
- });
32412
- this.overBudgetDrains.add(drain);
32413
- void drain.finally(() => { this.overBudgetDrains.delete(drain); });
32414
- }
33106
+ : this.kickOverBudgetDrain(agentName, agent);
32415
33107
  if (overBudget) {
32416
33108
  this.opsAlert('context-refusal', agentName, `compile refused (${errorType ?? 'unclassified'}): ${boundFailureText(wholeReason, FAILURE_SUMMARY_MAX_CHARS)} — ` +
32417
33109
  `drain breaker ${drainState}; ` +
@@ -32485,6 +33177,7 @@ export class AgentFramework {
32485
33177
  reason: 'inference-failure-rewind-retry',
32486
33178
  source: 'framework',
32487
33179
  timestamp: Date.now(),
33180
+ ...this.wakeChannelsMark(this.turnRouting?.get(agent.name)),
32488
33181
  });
32489
33182
  }
32490
33183
  // ==========================================================================
@@ -32667,7 +33360,8 @@ export class AgentFramework {
32667
33360
  // a later start of the turn is told where its unprefixed speech lands
32668
33361
  // unless a hybrid `>>>` destination takes it. Empty for a no-trigger
32669
33362
  // turn until its locus is resolved, so that resolution takes the
32670
- // global fallback (none for an ephemeral run), and the turn then keeps
33363
+ // global fallback, the configured unprompted channel among it (none
33364
+ // for an ephemeral run), and the turn then keeps
32671
33365
  // that channel; null for a heartbeat's turn, which takes none
32672
33366
  // (TurnRouting.heartbeat).
32673
33367
  // A turn with no channel of its own (a heartbeat's, or a wake for a
@@ -32687,7 +33381,9 @@ export class AgentFramework {
32687
33381
  // message's channel is nobody's choice for it (#551). By name, so a
32688
33382
  // speech dispatch that comes after the run's release still counts.
32689
33383
  takesGlobalFallback: (conversationId) => this.usedEphemeralAgentNames.get(conversationId) !== 'run',
33384
+ takesUnpromptedChannel: (conversationId) => this.openLogical.get(conversationId)?.unprompted === true,
32690
33385
  eventGatePresent: this.eventGate !== null,
33386
+ unpromptedChannel: this.mcplUnpromptedChannel ?? undefined,
32691
33387
  // A channel tool reads the connect and restart marks a tool call's
32692
33388
  // "connecting" text reads (mcplConnectInFlight, mcplServerAbsentText).
32693
33389
  // This host is connecting to a server while a connect or restart of
@@ -32708,7 +33404,7 @@ export class AgentFramework {
32708
33404
  // or wasn't confirmed to. addMessage() alone does not request
32709
33405
  // inference, so this never wakes her (matching the
32710
33406
  // `discord-send-failed-skip` gate intent: context yes, wake no).
32711
- onRouteFailure: ({ conversationId, channelId, serverId, reason, textLen, outcome, detail }) => {
33407
+ onRouteFailure: ({ conversationId, channelId, serverId, reason, textLen, outcome, detail, byDesign }) => {
32712
33408
  try {
32713
33409
  // The marker is `system: true`, so it is never conversational and
32714
33410
  // never influences routing. The channel is named by its label when
@@ -32738,7 +33434,7 @@ export class AgentFramework {
32738
33434
  // have been posted (in part or whole). Never claim it wasn't.
32739
33435
  ? `[${NOTICE_TAG.discordSendFailed}] Delivery of your previous reply (${textLen} chars) to ${where} was not confirmed (${suppliedValue(reason)}). It may or may not have been posted, so sending it again could post it twice. It is saved in your archive.`
32740
33436
  : `[${NOTICE_TAG.discordSendFailed}] Your previous reply (${textLen} chars) could not be delivered to ${where} (${suppliedValue(reason)}). Nothing was posted; it is saved in your archive.`)
32741
- : `[${NOTICE_TAG.sendUndeliverable}] Your previous reply (${textLen} chars) had no channel to go to — ${suppliedValue(reason)}. This is a routing/configuration situation, not a channel failure. This route delivered it nowhere; if another surface showed it (a reply thread, the console), that copy stands. It is saved in your archive.`;
33437
+ : `[${NOTICE_TAG.sendUndeliverable}] Your previous reply (${textLen} chars) had no channel to go to — ${suppliedValue(reason)}. This is a ${byDesign ? 'routing' : 'routing/configuration'} situation, not a channel failure. This route delivered it nowhere; if another surface showed it (a reply thread, the console), that copy stands. It is saved in your archive.`;
32742
33438
  // A registered speaker's notice is addressed to it (noticeTargetFor).
32743
33439
  this.addMessage('user', [{ type: 'text', text: text + (detail !== undefined ? ` Connector detail: ${suppliedValue(detail)}` : '') }], metadata, { forAgent: conversationId });
32744
33440
  }
@@ -35846,7 +36542,7 @@ export class AgentFramework {
35846
36542
  .filter(([, owner]) => owner === serverId)
35847
36543
  .map(([name]) => name);
35848
36544
  const known = new Set(names);
35849
- tools.push(...(this.mcplTools ?? []).filter((tool) => known.has(tool.name)));
36545
+ appendAll(tools, (this.mcplTools ?? []).filter((tool) => known.has(tool.name)));
35850
36546
  for (const name of names) {
35851
36547
  toolServers.set(name, serverId);
35852
36548
  const featureSet = this.mcplToolFeatureSets?.get(name);
@@ -35859,7 +36555,7 @@ export class AgentFramework {
35859
36555
  kept.set(serverId, names.length);
35860
36556
  }
35861
36557
  else if (listedServers.has(serverId)) {
35862
- tools.push(...staged.tools);
36558
+ appendAll(tools, staged.tools);
35863
36559
  for (const [name, featureSet] of staged.featureSets)
35864
36560
  toolFeatureSets.set(name, featureSet);
35865
36561
  for (const [name, classes] of staged.classes)
@@ -35876,7 +36572,7 @@ export class AgentFramework {
35876
36572
  console.error(`[mcpl] server "${serverId}": tools/list failed (${error}); ${count > 0 ? `keeping its ${count} known tool(s)` : 'none of its tools are offered'}`);
35877
36573
  }
35878
36574
  }
35879
- tools.push(...(over.size > 0 ? modernTools.filter((tool) => toolServers.has(tool.name)) : modernTools));
36575
+ appendAll(tools, over.size > 0 ? modernTools.filter((tool) => toolServers.has(tool.name)) : modernTools);
35880
36576
  this.mcplToolsRefreshPublished = generation;
35881
36577
  this.mcplTools = tools;
35882
36578
  this.mcplPublishedConnections = publishedConnections;
@@ -36810,6 +37506,11 @@ export class AgentFramework {
36810
37506
  return;
36811
37507
  }
36812
37508
  if (call.name === 'channel_publish' || call.name === 'channel_close') {
37509
+ const refusal = this.channelRegistry?.nonStringRefusal(call.name, call.input);
37510
+ if (refusal) {
37511
+ reject(refusal.error);
37512
+ return;
37513
+ }
36813
37514
  const input = (call.input ?? {});
36814
37515
  // An empty serverId is read as none given, as the channel tools and
36815
37516
  // channelNamed read it.
@@ -37342,7 +38043,7 @@ export class AgentFramework {
37342
38043
  .filter(([field]) => !(field.startsWith('behavior.') && typeof input.behavior === 'object')));
37343
38044
  const policy = this.eventGate.addPolicy(rawPolicy, {
37344
38045
  ...(position ? { position } : {}),
37345
- onIgnoredFields: (notes) => { ignored.push(...notes); },
38046
+ onIgnoredFields: (notes) => { appendAll(ignored, notes); },
37346
38047
  fieldNames,
37347
38048
  });
37348
38049
  const after = this.eventGate.probeTable();
@@ -37667,8 +38368,9 @@ export class AgentFramework {
37667
38368
  /**
37668
38369
  * Whether `agent`'s view leaves suppressed rows out, asked of the view
37669
38370
  * itself (storeSees): every resident's, the subconscious's and a view
37670
- * derived from one does; an isolated agent's own store has no view filter,
37671
- * so a row stamped there would render.
38371
+ * derived from one does. An isolated agent's own view excludes legacy
38372
+ * reasoning-loss notices, not suppressed rows, so a suppressed row stamped
38373
+ * there would render.
37672
38374
  */
37673
38375
  viewHidesSuppressed(agent) {
37674
38376
  if (!agent)
@@ -37732,6 +38434,7 @@ export class AgentFramework {
37732
38434
  tags: Array.isArray(e.gateMetadata.tags) ? e.gateMetadata.tags : undefined,
37733
38435
  ...(e.targetAgents !== undefined ? { recipients: e.targetAgents.filter((name) => this.agents.has(name)) } : {}),
37734
38436
  ...(asked ? { suppressionAsked: true } : {}),
38437
+ ...(opts.folds ? { folds: true } : {}),
37735
38438
  });
37736
38439
  const decision = admit(env, {
37737
38440
  untargetedRecipients: () => this.registry.untargetedRecipients(),
@@ -38085,7 +38788,7 @@ export class AgentFramework {
38085
38788
  }
38086
38789
  else {
38087
38790
  const locus = callingTurn.locus ?? null;
38088
- registry.routeSpeech(agentName, announcement(locus), locus).then(noted).catch((err) => {
38791
+ registry.routeSpeech(agentName, announcement(locus), locus, this.cameFrom(callingTurn, locus)).then(noted).catch((err) => {
38089
38792
  console.error('[sleep] announce failed:', err instanceof Error ? err.message : err);
38090
38793
  });
38091
38794
  }