@animalabs/agent-framework 0.31.0 → 0.33.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 (298) hide show
  1. package/README.md +204 -14
  2. package/dist/src/admission/admit.d.ts +18 -0
  3. package/dist/src/admission/admit.d.ts.map +1 -1
  4. package/dist/src/admission/admit.js +16 -3
  5. package/dist/src/admission/admit.js.map +1 -1
  6. package/dist/src/agent.d.ts +185 -78
  7. package/dist/src/agent.d.ts.map +1 -1
  8. package/dist/src/agent.js +279 -102
  9. package/dist/src/agent.js.map +1 -1
  10. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  11. package/dist/src/api/mcp-server-core.js +50 -12
  12. package/dist/src/api/mcp-server-core.js.map +1 -1
  13. package/dist/src/api/server.d.ts.map +1 -1
  14. package/dist/src/api/server.js +4 -20
  15. package/dist/src/api/server.js.map +1 -1
  16. package/dist/src/code-execution/isolate/scripts.d.ts +12 -8
  17. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/scripts.js +24 -0
  19. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  20. package/dist/src/config-checks.d.ts +7 -0
  21. package/dist/src/config-checks.d.ts.map +1 -1
  22. package/dist/src/config-checks.js +14 -0
  23. package/dist/src/config-checks.js.map +1 -1
  24. package/dist/src/context-manager-errors.d.ts +25 -0
  25. package/dist/src/context-manager-errors.d.ts.map +1 -0
  26. package/dist/src/context-manager-errors.js +31 -0
  27. package/dist/src/context-manager-errors.js.map +1 -0
  28. package/dist/src/context-options.d.ts +67 -0
  29. package/dist/src/context-options.d.ts.map +1 -0
  30. package/dist/src/context-options.js +85 -0
  31. package/dist/src/context-options.js.map +1 -0
  32. package/dist/src/deferred-import.d.ts +93 -0
  33. package/dist/src/deferred-import.d.ts.map +1 -0
  34. package/dist/src/deferred-import.js +177 -0
  35. package/dist/src/deferred-import.js.map +1 -0
  36. package/dist/src/dendrite/audience.d.ts +223 -0
  37. package/dist/src/dendrite/audience.d.ts.map +1 -0
  38. package/dist/src/dendrite/audience.js +202 -0
  39. package/dist/src/dendrite/audience.js.map +1 -0
  40. package/dist/src/dendrite/index.d.ts +5 -3
  41. package/dist/src/dendrite/index.d.ts.map +1 -1
  42. package/dist/src/dendrite/index.js +2 -1
  43. package/dist/src/dendrite/index.js.map +1 -1
  44. package/dist/src/dendrite/presets.d.ts +10 -5
  45. package/dist/src/dendrite/presets.d.ts.map +1 -1
  46. package/dist/src/dendrite/presets.js +17 -5
  47. package/dist/src/dendrite/presets.js.map +1 -1
  48. package/dist/src/dendrite/registry.d.ts +62 -7
  49. package/dist/src/dendrite/registry.d.ts.map +1 -1
  50. package/dist/src/dendrite/registry.js +179 -10
  51. package/dist/src/dendrite/registry.js.map +1 -1
  52. package/dist/src/dendrite/types.d.ts +91 -8
  53. package/dist/src/dendrite/types.d.ts.map +1 -1
  54. package/dist/src/foreign-rounds.d.ts +69 -0
  55. package/dist/src/foreign-rounds.d.ts.map +1 -0
  56. package/dist/src/foreign-rounds.js +152 -0
  57. package/dist/src/foreign-rounds.js.map +1 -0
  58. package/dist/src/framework.d.ts +1977 -820
  59. package/dist/src/framework.d.ts.map +1 -1
  60. package/dist/src/framework.js +7571 -3457
  61. package/dist/src/framework.js.map +1 -1
  62. package/dist/src/gate/event-gate.d.ts +85 -15
  63. package/dist/src/gate/event-gate.d.ts.map +1 -1
  64. package/dist/src/gate/event-gate.js +421 -47
  65. package/dist/src/gate/event-gate.js.map +1 -1
  66. package/dist/src/gate/index.d.ts +1 -1
  67. package/dist/src/gate/index.d.ts.map +1 -1
  68. package/dist/src/gate/index.js.map +1 -1
  69. package/dist/src/gate/rule-scope.d.ts +30 -0
  70. package/dist/src/gate/rule-scope.d.ts.map +1 -0
  71. package/dist/src/gate/rule-scope.js +41 -0
  72. package/dist/src/gate/rule-scope.js.map +1 -0
  73. package/dist/src/gate/suppress-script-worker.d.ts +2 -0
  74. package/dist/src/gate/suppress-script-worker.d.ts.map +1 -0
  75. package/dist/src/gate/suppress-script-worker.js +103 -0
  76. package/dist/src/gate/suppress-script-worker.js.map +1 -0
  77. package/dist/src/gate/suppress-scripts.d.ts +105 -0
  78. package/dist/src/gate/suppress-scripts.d.ts.map +1 -0
  79. package/dist/src/gate/suppress-scripts.js +403 -0
  80. package/dist/src/gate/suppress-scripts.js.map +1 -0
  81. package/dist/src/gate/suppression-review.d.ts +143 -0
  82. package/dist/src/gate/suppression-review.d.ts.map +1 -0
  83. package/dist/src/gate/suppression-review.js +463 -0
  84. package/dist/src/gate/suppression-review.js.map +1 -0
  85. package/dist/src/gate/suppression-stats.d.ts +125 -0
  86. package/dist/src/gate/suppression-stats.d.ts.map +1 -0
  87. package/dist/src/gate/suppression-stats.js +214 -0
  88. package/dist/src/gate/suppression-stats.js.map +1 -0
  89. package/dist/src/gate/types.d.ts +135 -7
  90. package/dist/src/gate/types.d.ts.map +1 -1
  91. package/dist/src/gate/wake-onboarding.d.ts +119 -0
  92. package/dist/src/gate/wake-onboarding.d.ts.map +1 -0
  93. package/dist/src/gate/wake-onboarding.js +277 -0
  94. package/dist/src/gate/wake-onboarding.js.map +1 -0
  95. package/dist/src/held-writes.d.ts +31 -23
  96. package/dist/src/held-writes.d.ts.map +1 -1
  97. package/dist/src/held-writes.js +44 -59
  98. package/dist/src/held-writes.js.map +1 -1
  99. package/dist/src/index.d.ts +5 -2
  100. package/dist/src/index.d.ts.map +1 -1
  101. package/dist/src/index.js +3 -0
  102. package/dist/src/index.js.map +1 -1
  103. package/dist/src/jsonrpc-envelope.d.ts +69 -0
  104. package/dist/src/jsonrpc-envelope.d.ts.map +1 -0
  105. package/dist/src/jsonrpc-envelope.js +66 -0
  106. package/dist/src/jsonrpc-envelope.js.map +1 -0
  107. package/dist/src/mcpl/channel-registry.d.ts +131 -15
  108. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  109. package/dist/src/mcpl/channel-registry.js +290 -46
  110. package/dist/src/mcpl/channel-registry.js.map +1 -1
  111. package/dist/src/mcpl/checkpoint-manager.d.ts +2 -4
  112. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -1
  113. package/dist/src/mcpl/checkpoint-manager.js.map +1 -1
  114. package/dist/src/mcpl/feature-set-manager.d.ts +2 -4
  115. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -1
  116. package/dist/src/mcpl/feature-set-manager.js.map +1 -1
  117. package/dist/src/mcpl/inbound-source.d.ts +2 -1
  118. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  119. package/dist/src/mcpl/inbound-source.js +2 -1
  120. package/dist/src/mcpl/inbound-source.js.map +1 -1
  121. package/dist/src/mcpl/inference-router.d.ts +6 -6
  122. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  123. package/dist/src/mcpl/inference-router.js.map +1 -1
  124. package/dist/src/mcpl/push-coalescer.d.ts +132 -22
  125. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  126. package/dist/src/mcpl/push-coalescer.js +105 -15
  127. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  128. package/dist/src/mcpl/push-handler.d.ts +9 -7
  129. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  130. package/dist/src/mcpl/push-handler.js.map +1 -1
  131. package/dist/src/mcpl/server-connection.d.ts +13 -10
  132. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  133. package/dist/src/mcpl/server-connection.js +134 -56
  134. package/dist/src/mcpl/server-connection.js.map +1 -1
  135. package/dist/src/mcpl/tool-classes.d.ts.map +1 -1
  136. package/dist/src/mcpl/tool-classes.js +3 -0
  137. package/dist/src/mcpl/tool-classes.js.map +1 -1
  138. package/dist/src/mcpl/types.d.ts +8 -2
  139. package/dist/src/mcpl/types.d.ts.map +1 -1
  140. package/dist/src/mcpl/types.js.map +1 -1
  141. package/dist/src/module-registry.d.ts +24 -1
  142. package/dist/src/module-registry.d.ts.map +1 -1
  143. package/dist/src/module-registry.js +68 -18
  144. package/dist/src/module-registry.js.map +1 -1
  145. package/dist/src/modules/discord/index.d.ts +45 -8
  146. package/dist/src/modules/discord/index.d.ts.map +1 -1
  147. package/dist/src/modules/discord/index.js +237 -86
  148. package/dist/src/modules/discord/index.js.map +1 -1
  149. package/dist/src/modules/history/index.d.ts +59 -0
  150. package/dist/src/modules/history/index.d.ts.map +1 -1
  151. package/dist/src/modules/history/index.js +432 -67
  152. package/dist/src/modules/history/index.js.map +1 -1
  153. package/dist/src/modules/history/semantic.d.ts +6 -5
  154. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  155. package/dist/src/modules/history/semantic.js +15 -22
  156. package/dist/src/modules/history/semantic.js.map +1 -1
  157. package/dist/src/modules/workspace/disk-agreement.d.ts +1 -1
  158. package/dist/src/modules/workspace/disk-agreement.js +1 -1
  159. package/dist/src/modules/workspace/index.d.ts +28 -7
  160. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  161. package/dist/src/modules/workspace/index.js +76 -22
  162. package/dist/src/modules/workspace/index.js.map +1 -1
  163. package/dist/src/notice-tags.d.ts +10 -1
  164. package/dist/src/notice-tags.d.ts.map +1 -1
  165. package/dist/src/notice-tags.js +10 -1
  166. package/dist/src/notice-tags.js.map +1 -1
  167. package/dist/src/operator-log.d.ts +36 -3
  168. package/dist/src/operator-log.d.ts.map +1 -1
  169. package/dist/src/operator-log.js +2 -2
  170. package/dist/src/operator-log.js.map +1 -1
  171. package/dist/src/presentation.d.ts +746 -0
  172. package/dist/src/presentation.d.ts.map +1 -0
  173. package/dist/src/presentation.js +1435 -0
  174. package/dist/src/presentation.js.map +1 -0
  175. package/dist/src/prose-drafts.d.ts +15 -11
  176. package/dist/src/prose-drafts.d.ts.map +1 -1
  177. package/dist/src/prose-drafts.js +16 -12
  178. package/dist/src/prose-drafts.js.map +1 -1
  179. package/dist/src/read-marks.d.ts +192 -0
  180. package/dist/src/read-marks.d.ts.map +1 -0
  181. package/dist/src/read-marks.js +242 -0
  182. package/dist/src/read-marks.js.map +1 -0
  183. package/dist/src/recovery/offline-branch.d.ts +91 -6
  184. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  185. package/dist/src/recovery/offline-branch.js +273 -29
  186. package/dist/src/recovery/offline-branch.js.map +1 -1
  187. package/dist/src/recovery/recover-cli.js +22 -3
  188. package/dist/src/recovery/recover-cli.js.map +1 -1
  189. package/dist/src/send-tools.d.ts +5 -9
  190. package/dist/src/send-tools.d.ts.map +1 -1
  191. package/dist/src/send-tools.js +40 -7
  192. package/dist/src/send-tools.js.map +1 -1
  193. package/dist/src/stored-injections.d.ts +2 -2
  194. package/dist/src/stored-injections.js +2 -2
  195. package/dist/src/stored-removal.d.ts +17 -4
  196. package/dist/src/stored-removal.d.ts.map +1 -1
  197. package/dist/src/stored-removal.js +29 -5
  198. package/dist/src/stored-removal.js.map +1 -1
  199. package/dist/src/tool-result-guard.d.ts +12 -1
  200. package/dist/src/tool-result-guard.d.ts.map +1 -1
  201. package/dist/src/tool-result-guard.js +42 -8
  202. package/dist/src/tool-result-guard.js.map +1 -1
  203. package/dist/src/tune-out/coordinator.d.ts +1159 -79
  204. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  205. package/dist/src/tune-out/coordinator.js +3709 -363
  206. package/dist/src/tune-out/coordinator.js.map +1 -1
  207. package/dist/src/tune-out/tools.d.ts +114 -17
  208. package/dist/src/tune-out/tools.d.ts.map +1 -1
  209. package/dist/src/tune-out/tools.js +87 -27
  210. package/dist/src/tune-out/tools.js.map +1 -1
  211. package/dist/src/tune-out/window-anchor.d.ts +143 -0
  212. package/dist/src/tune-out/window-anchor.d.ts.map +1 -0
  213. package/dist/src/tune-out/window-anchor.js +202 -0
  214. package/dist/src/tune-out/window-anchor.js.map +1 -0
  215. package/dist/src/types/agent.d.ts +86 -31
  216. package/dist/src/types/agent.d.ts.map +1 -1
  217. package/dist/src/types/events.d.ts +2 -0
  218. package/dist/src/types/events.d.ts.map +1 -1
  219. package/dist/src/types/framework.d.ts +43 -21
  220. package/dist/src/types/framework.d.ts.map +1 -1
  221. package/dist/src/types/module.d.ts +180 -69
  222. package/dist/src/types/module.d.ts.map +1 -1
  223. package/dist/src/types/trace.d.ts +510 -36
  224. package/dist/src/types/trace.d.ts.map +1 -1
  225. package/package.json +3 -3
  226. package/src/admission/admit.ts +32 -3
  227. package/src/agent.ts +323 -120
  228. package/src/api/mcp-server-core.ts +54 -17
  229. package/src/api/server.ts +4 -23
  230. package/src/code-execution/isolate/scripts.ts +28 -3
  231. package/src/config-checks.ts +14 -0
  232. package/src/context-manager-errors.ts +37 -0
  233. package/src/context-options.ts +96 -0
  234. package/src/deferred-import.ts +239 -0
  235. package/src/dendrite/audience.ts +315 -0
  236. package/src/dendrite/index.ts +14 -2
  237. package/src/dendrite/presets.ts +18 -6
  238. package/src/dendrite/registry.ts +209 -12
  239. package/src/dendrite/types.ts +92 -8
  240. package/src/foreign-rounds.ts +199 -0
  241. package/src/framework.ts +7814 -3585
  242. package/src/gate/event-gate.ts +428 -65
  243. package/src/gate/index.ts +3 -0
  244. package/src/gate/rule-scope.ts +45 -0
  245. package/src/gate/suppress-script-worker.ts +89 -0
  246. package/src/gate/suppress-scripts.ts +414 -0
  247. package/src/gate/suppression-review.ts +517 -0
  248. package/src/gate/suppression-stats.ts +314 -0
  249. package/src/gate/types.ts +141 -7
  250. package/src/gate/wake-onboarding.ts +309 -0
  251. package/src/held-writes.ts +49 -60
  252. package/src/index.ts +9 -2
  253. package/src/jsonrpc-envelope.ts +81 -0
  254. package/src/mcpl/channel-registry.ts +353 -85
  255. package/src/mcpl/checkpoint-manager.ts +3 -2
  256. package/src/mcpl/feature-set-manager.ts +2 -1
  257. package/src/mcpl/inbound-source.ts +2 -1
  258. package/src/mcpl/inference-router.ts +7 -4
  259. package/src/mcpl/push-coalescer.ts +193 -34
  260. package/src/mcpl/push-handler.ts +10 -5
  261. package/src/mcpl/server-connection.ts +141 -60
  262. package/src/mcpl/tool-classes.ts +3 -0
  263. package/src/mcpl/types.ts +8 -2
  264. package/src/module-registry.ts +73 -17
  265. package/src/modules/discord/index.ts +229 -81
  266. package/src/modules/history/index.ts +439 -70
  267. package/src/modules/history/semantic.ts +15 -21
  268. package/src/modules/workspace/disk-agreement.ts +1 -1
  269. package/src/modules/workspace/index.ts +85 -23
  270. package/src/notice-tags.ts +10 -1
  271. package/src/operator-log.ts +36 -3
  272. package/src/presentation.ts +1676 -0
  273. package/src/prose-drafts.ts +16 -12
  274. package/src/read-marks.ts +328 -0
  275. package/src/recovery/offline-branch.ts +346 -32
  276. package/src/recovery/recover-cli.ts +21 -3
  277. package/src/send-tools.ts +40 -7
  278. package/src/stored-injections.ts +2 -2
  279. package/src/stored-removal.ts +31 -7
  280. package/src/tool-result-guard.ts +41 -7
  281. package/src/tune-out/coordinator.ts +4089 -457
  282. package/src/tune-out/tools.ts +160 -43
  283. package/src/tune-out/window-anchor.ts +277 -0
  284. package/src/types/agent.ts +90 -31
  285. package/src/types/events.ts +2 -0
  286. package/src/types/framework.ts +43 -21
  287. package/src/types/module.ts +181 -68
  288. package/src/types/trace.ts +542 -40
  289. package/dist/src/reasoning-loss-notice.d.ts +0 -23
  290. package/dist/src/reasoning-loss-notice.d.ts.map +0 -1
  291. package/dist/src/reasoning-loss-notice.js +0 -52
  292. package/dist/src/reasoning-loss-notice.js.map +0 -1
  293. package/dist/src/reasoning-loss.d.ts +0 -24
  294. package/dist/src/reasoning-loss.d.ts.map +0 -1
  295. package/dist/src/reasoning-loss.js +0 -102
  296. package/dist/src/reasoning-loss.js.map +0 -1
  297. package/src/reasoning-loss-notice.ts +0 -64
  298. package/src/reasoning-loss.ts +0 -120
@@ -26,6 +26,7 @@ import { CapabilityGrant } from './capability-grant.js';
26
26
  import { McplRequestError } from './server-connection.js';
27
27
  import { readSkipReplyWake, skipReplyNote } from './skip-reply-wake.js';
28
28
  import { isProseTargetToken } from './prose-grammar.js';
29
+ import { persistentReaderName, tuneOutHeldWords, tuneOutNoReaderAnswer, tuneOutReachWords } from '../tune-out/tools.js';
29
30
  // ============================================================================
30
31
  // Typing indicator interval (Discord typing lasts ~10s, so 7s keeps it alive)
31
32
  // ============================================================================
@@ -146,8 +147,8 @@ function desiredStateRecord(item) {
146
147
  }
147
148
  /**
148
149
  * The desired state `store`'s lifecycle log records for `channelId` on
149
- * `serverId`, for a reader with no registry yet, as a serving boot's
150
- * deferred-write flush is, before initializeMcpl: the last decision wins, as
150
+ * `serverId`, for a reader with no registry yet, as the boot's import of an
151
+ * earlier release's queued writes is, before initializeMcpl: the last decision wins, as
151
152
  * the registry's replay reads it (getDesiredState answers once there is a
152
153
  * registry). undefined when no decision names it.
153
154
  */
@@ -478,6 +479,32 @@ function withEmbeddedTextMarked(block) {
478
479
  function channelOnServer(ref) {
479
480
  return `${quotedValue(ref.channelId)} on server ${quotedValue(ref.serverId)}`;
480
481
  }
482
+ /** A channel_close's lead where its channel's server no longer registers it. */
483
+ function noLongerRegistered(ref) {
484
+ return `Server ${quotedValue(ref.serverId)} no longer registers channel ${quotedValue(ref.channelId)}, so no close was recorded or sent.`;
485
+ }
486
+ /** What a channel_close's bare name reached besides the channel it closed
487
+ * (ChannelRegistry.focusNamedBare): each by its id and server, which the
488
+ * bare name leaves out, and the channelId and serverId a call closes it
489
+ * with: its own id, which needn't be the name the call gave (a focus on
490
+ * `b:room` beside server b's `room`), so naming the server alone, the
491
+ * name kept, is no channel there. */
492
+ function notClosedText(refs) {
493
+ const names = refs.map(channelOnServer);
494
+ const call = (ref) => `channelId ${quotedValue(ref.channelId)} and serverId ${quotedValue(ref.serverId)}`;
495
+ return refs.length === 1
496
+ ? `Channel ${names[0]} was not closed: to close it, call again with ${call(refs[0])}.`
497
+ : `Channels ${names.slice(0, -1).join(', ')} and ${names.at(-1)} were not closed: to close one, call again with ` +
498
+ `${refs.slice(0, -1).map(call).join(', with ')}, or with ${call(refs.at(-1))}.`;
499
+ }
500
+ /** A tool result with a sentence added: a success's after its note (or as
501
+ * its note), a failure's after its error. */
502
+ function withSentence(result, sentence) {
503
+ if (!result.success)
504
+ return { ...result, error: result.error ? `${result.error} ${sentence}` : sentence };
505
+ const data = (result.data ?? {});
506
+ return { ...result, data: { ...data, note: typeof data.note === 'string' ? `${data.note} ${sentence}` : sentence } };
507
+ }
481
508
  const CHANNEL_ON_SERVER_AT_END = /("(?:[^"\\]|\\.)*") on server ("(?:[^"\\]|\\.)*")$/;
482
509
  /**
483
510
  * A failed request's words, bounded, for a reason that a caller composes.
@@ -680,10 +707,12 @@ export class ChannelRegistry {
680
707
  latestInboundLocus;
681
708
  onToolClose;
682
709
  focusChannelResolver;
710
+ tuneOutReaderFn;
683
711
  onRouteFailure;
684
712
  onChannelAutoOpened;
685
713
  homeChannelResolver;
686
714
  activeChannelResolver;
715
+ channellessTurn;
687
716
  takesGlobalFallback;
688
717
  serverConnecting;
689
718
  store;
@@ -934,17 +963,17 @@ export class ChannelRegistry {
934
963
  (reach === 'gone' ? ' If you need the server back, ask whoever runs this host.' : '') +
935
964
  (failure === undefined ? ` ${withStop(lifecycleClause(err))}` : '');
936
965
  }
937
- // A tune-out sends the channel's messages to the subconscious, and
966
+ // A tune-out holds the channel's messages for its reader, if any, and
938
967
  // channel_open refuses a tuned-out channel.
939
968
  if (action === 'open' ? now !== 'open' : now !== 'closed') {
940
969
  if (action === 'open' && now === 'tuned-out') {
941
- return `Channel ${channel} was tuned out meanwhile: its messages go to the subconscious, and channel_open doesn't ` +
970
+ return `Channel ${channel} was tuned out meanwhile: its messages ${this.tuneOutHeld()}, and channel_open doesn't ` +
942
971
  `change that. ${this.tuneOutEndText(current)}`;
943
972
  }
944
973
  // channel_close refuses a tuned-out channel too, so calling it again
945
974
  // isn't the way: the tune-out's cancel is, and it delivers the backlog.
946
975
  if (action === 'close' && now === 'tuned-out') {
947
- return `Channel ${channel} was tuned out meanwhile: its messages go to the subconscious, and channel_close doesn't ` +
976
+ return `Channel ${channel} was tuned out meanwhile: its messages ${this.tuneOutHeld()}, and channel_close doesn't ` +
948
977
  `change that. ${this.tuneOutEndText(current, 'channel_close')}`;
949
978
  }
950
979
  // The later decision's own work settles the transport, and its call
@@ -1053,10 +1082,12 @@ export class ChannelRegistry {
1053
1082
  this.latestInboundLocus = options?.latestInboundLocus === true;
1054
1083
  this.onToolClose = options?.onToolClose;
1055
1084
  this.focusChannelResolver = options?.focusChannelResolver;
1085
+ this.tuneOutReaderFn = options?.tuneOutReader;
1056
1086
  this.onRouteFailure = options?.onRouteFailure;
1057
1087
  this.onChannelAutoOpened = options?.onChannelAutoOpened;
1058
1088
  this.homeChannelResolver = options?.homeChannelResolver;
1059
1089
  this.activeChannelResolver = options?.activeChannelResolver;
1090
+ this.channellessTurn = options?.channellessTurn;
1060
1091
  this.takesGlobalFallback = options?.takesGlobalFallback;
1061
1092
  this.serverConnecting = options?.serverConnecting;
1062
1093
  this.store = options?.store;
@@ -1686,8 +1717,10 @@ export class ChannelRegistry {
1686
1717
  }
1687
1718
  }
1688
1719
  /** The explicit 'stop' for a channel whose typing ended, while its
1689
- * server still registers it. */
1720
+ * server still registers it and holds the grant its start needed. */
1690
1721
  sendTypingStop(ref) {
1722
+ if (!this.typingGranted(ref))
1723
+ return;
1691
1724
  if (this.sendTypingFn && this.entryAt(ref)) {
1692
1725
  this.sendTypingFn(ref.serverId, ref.channelId, this.typingMetadata.get(destinationKey(ref)), 'stop');
1693
1726
  }
@@ -1858,7 +1891,9 @@ export class ChannelRegistry {
1858
1891
  * its home.
1859
1892
  */
1860
1893
  incomingFor(agentName) {
1861
- if (agentName !== undefined && this.activeChannelResolver?.(agentName) === null && !this.homeChannelResolver?.(agentName)) {
1894
+ const channelless = agentName === undefined ? false
1895
+ : this.channellessTurn ? this.channellessTurn(agentName) : this.activeChannelResolver?.(agentName) === null;
1896
+ if (channelless && !this.homeChannelResolver?.(agentName)) {
1862
1897
  return undefined;
1863
1898
  }
1864
1899
  // The latest delivered inbound itself, not the speech fallback
@@ -2466,7 +2501,7 @@ export class ChannelRegistry {
2466
2501
  }
2467
2502
  /**
2468
2503
  * Make a tuned-out channel's transport match its tune-out, which keeps it
2469
- * open so its traffic reaches the subconscious (TuneOutParams). The
2504
+ * open so its traffic reaches its reader, or is held (TuneOutParams). The
2470
2505
  * tune-out is recorded first (enterTuneOut), and this writes no decision,
2471
2506
  * so the tune-out stands whatever becomes of the open. Lifecycle work
2472
2507
  * queued for the channel goes first, until none is left, since each
@@ -2478,8 +2513,8 @@ export class ChannelRegistry {
2478
2513
  * tune-out as open), and the answer waits for the work queued after it
2479
2514
  * too, such as a registration that came back, and comes from the state
2480
2515
  * that work leaves. Returns nothing where the transport is open, or, for
2481
- * tune_out's answer, why the channel's messages don't or may not reach the
2482
- * subconscious yet and when they will, or that the tune-out isn't in
2516
+ * tune_out's answer, why the channel's messages don't or may not reach its
2517
+ * reader yet, or be received, and when they will, or that the tune-out isn't in
2483
2518
  * effect any more. A call while this tune-out's open is in flight, as a
2484
2519
  * repeated tune_out makes, gets that open's answer rather than send
2485
2520
  * another.
@@ -2545,8 +2580,9 @@ export class ChannelRegistry {
2545
2580
  // that took effect would have its traffic diverted. `again` where calling
2546
2581
  // tune_out again tries the open now: the channel is registered here and
2547
2582
  // its server up.
2548
- const notYet = (later, why, again = false) => withStop(`The tune-out is recorded, but its messages may not reach the subconscious until this host opens the channel, ` +
2549
- `which it does ${later}${again ? ', or now if you call tune_out again' : ''}; ${why}`);
2583
+ // Where no reader runs, each answer is that alone (tuneOutNoReaderAnswer).
2584
+ const notYet = (later, why, again = false) => tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? withStop(`The tune-out is recorded, but its messages ${this.tuneOutReach('may not reach')} until this host opens the ` +
2585
+ `channel, which it does ${later}${again ? ', or now if you call tune_out again' : ''}; ${why}`);
2550
2586
  const nextRegistration = `when the "${serverId}" server next registers this channel, if this host may open and ` +
2551
2587
  'close its channels then';
2552
2588
  // The work queued for the channel as it stands: its tail, waited for,
@@ -2580,16 +2616,16 @@ export class ChannelRegistry {
2580
2616
  // established.
2581
2617
  const reach = this.serverReach(serverId);
2582
2618
  if (reach !== 'up') {
2583
- return withStop(`The tune-out is recorded, but its messages don't reach the subconscious until the "${serverId}" server is ` +
2584
- `connected and the channel is open; it can't be opened now: ${unreachableClause(serverId, reach)}` +
2619
+ return tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? withStop(`The tune-out is recorded, but its messages ${this.tuneOutReach("don't reach")} until the "${serverId}" server ` +
2620
+ `is connected and the channel is open; it can't be opened now: ${unreachableClause(serverId, reach)}` +
2585
2621
  (reach === 'gone' ? unreachableRemedy(reach) : ''));
2586
2622
  }
2587
2623
  const transportOpen = this.tuneOutReadsOpen(entry);
2588
2624
  if (!this.directsLifecycle(serverId)) {
2589
2625
  return transportOpen ? undefined
2590
- : `The tune-out is recorded, but ${this.lifecycleRefusal(serverId).message}, so its messages reach the ` +
2591
- 'subconscious only as that server sends them, and calling tune_out again won\'t change that. If you need ' +
2592
- 'this host to open it, ask whoever runs this host.';
2626
+ : tuneOutNoReaderAnswer(this.tuneOutReaderNow()) ?? `The tune-out is recorded, but ${this.lifecycleRefusal(serverId).message}, so its messages ` +
2627
+ `${this.tuneOutReach('reach')} only as that server sends them, and calling tune_out again won't change ` +
2628
+ 'that. If you need this host to open it, ask whoever runs this host.';
2593
2629
  }
2594
2630
  if (!entry)
2595
2631
  return missing;
@@ -3222,10 +3258,9 @@ export class ChannelRegistry {
3222
3258
  * (typing timer lifecycle is still managed for when the callback is wired).
3223
3259
  */
3224
3260
  sendTypingNotification(ref) {
3225
- // §14.1: channels/typing requires channels.typing in the grant. Silent
3226
- // skip — a typing indicator is cosmetic and per-7s, so a diagnostic per
3227
- // tick would be noise.
3228
- if (!CapabilityGrant.of(this.serverRegistry.getServer(ref.serverId)).has('channels.typing'))
3261
+ // Silent skip — a typing indicator is cosmetic and per-7s, so a
3262
+ // diagnostic per tick would be noise.
3263
+ if (!this.typingGranted(ref))
3229
3264
  return;
3230
3265
  if (this.sendTypingFn) {
3231
3266
  this.sendTypingFn(ref.serverId, ref.channelId, this.typingMetadata.get(destinationKey(ref)));
@@ -3233,6 +3268,10 @@ export class ChannelRegistry {
3233
3268
  // TODO: When server-connection exposes a public sendNotification or
3234
3269
  // sendTyping method, wire it here directly instead of using a callback.
3235
3270
  }
3271
+ /** §14.1: channels/typing, start and stop alike, requires channels.typing in the grant. */
3272
+ typingGranted(ref) {
3273
+ return CapabilityGrant.of(this.serverRegistry.getServer(ref.serverId)).has('channels.typing');
3274
+ }
3236
3275
  // ==========================================================================
3237
3276
  // Private: Channel Lookup
3238
3277
  // ==========================================================================
@@ -3305,6 +3344,18 @@ export class ChannelRegistry {
3305
3344
  }
3306
3345
  return channelOnServer(ref);
3307
3346
  }
3347
+ /** The reader of a tuned-out channel's traffic, as the host says (tuneOutReader), or the default persistent reader. */
3348
+ tuneOutReaderNow() {
3349
+ return this.tuneOutReaderFn?.() ?? { kind: 'persistent', name: persistentReaderName({}) };
3350
+ }
3351
+ /** What a tuned-out channel's messages are, after "its messages" (tuneOutHeldWords). */
3352
+ tuneOutHeld() {
3353
+ return tuneOutHeldWords(this.tuneOutReaderNow());
3354
+ }
3355
+ /** Whether a tuned-out channel's messages get to its reader, after "its messages" (tuneOutReachWords). */
3356
+ tuneOutReach(form) {
3357
+ return tuneOutReachWords(this.tuneOutReaderNow(), form);
3358
+ }
3308
3359
  /**
3309
3360
  * A tool's refusal of a tuned-out channel, whose decision is the tune-out:
3310
3361
  * its cancel is what ends it (tuneOutEndText), and a decision recorded over
@@ -3315,7 +3366,7 @@ export class ChannelRegistry {
3315
3366
  return {
3316
3367
  success: false,
3317
3368
  isError: true,
3318
- error: `Channel ${this.quotedShown(refOf(entry))} is tuned out: its messages go to the subconscious, and ${tool} ` +
3369
+ error: `Channel ${this.quotedShown(refOf(entry))} is tuned out: its messages ${this.tuneOutHeld()}, and ${tool} ` +
3319
3370
  `doesn't change that. ${this.tuneOutEndText(entry, tool)}`,
3320
3371
  data: { refusal: 'tuned-out', channelId: entry.descriptor.id, serverId: entry.serverId },
3321
3372
  };
@@ -3334,7 +3385,7 @@ export class ChannelRegistry {
3334
3385
  const entry = this.entryAt(ref);
3335
3386
  return entry ? this.tuneOutEndText(entry) : undefined;
3336
3387
  }
3337
- tuneOutEndText(entry, tool) {
3388
+ tuneOutEndText(entry, tool, closeWith) {
3338
3389
  const close = tool === 'channel_close';
3339
3390
  // A name is written as a literal tune_out's channelId takes back
3340
3391
  // (quotedValue, main's #852); one no name picks alone, by its id and
@@ -3344,7 +3395,7 @@ export class ChannelRegistry {
3344
3395
  ? `channelId ${quotedValue(name)}`
3345
3396
  : `channelId ${quotedValue(entry.descriptor.id)}, serverId ${JSON.stringify(entry.serverId)}`;
3346
3397
  return `Call tune_out with ${names} and mode "cancel" for it to resume normal ` +
3347
- `attention; cancelling delivers the diverted backlog${close ? ', and channel_close closes it after that.' : '.'}`;
3398
+ `attention; cancelling delivers the diverted backlog${close ? `, and channel_close${closeWith ? ` with ${closeWith}` : ''} closes it after that.` : '.'}`;
3348
3399
  }
3349
3400
  /**
3350
3401
  * A channel's name as nameShown gives it, quoted for a sentence: the name
@@ -4336,15 +4387,38 @@ export class ChannelRegistry {
4336
4387
  }
4337
4388
  }
4338
4389
  async handleToolClose(input, origin) {
4339
- const resolved = this.resolveToolChannelEntry(input.channelId, input.serverId, origin?.kind === 'agent' ? origin.own : undefined);
4340
- const entry = resolved.entry;
4341
- if (!entry) {
4342
- return {
4343
- success: false,
4344
- error: resolved.error,
4345
- isError: true,
4346
- };
4390
+ const own = origin?.kind === 'agent' ? origin.own : undefined;
4391
+ // An agent's bare name for the channel the all-except hold excepts means
4392
+ // that channel (focusNamedBare), whether its server still registers it
4393
+ // or not: another server's channel with the id, which the name would
4394
+ // reach otherwise, isn't closed, and the answer says so. A module's
4395
+ // close resolves its name as any other: housekeeping doesn't end the
4396
+ // resident's focus.
4397
+ const focused = origin?.kind === 'module' ? undefined : this.focusNamedBare(input.channelId, input.serverId, own);
4398
+ if (focused && 'refusal' in focused)
4399
+ return { success: false, error: focused.refusal, isError: true };
4400
+ if (focused) {
4401
+ const entry = this.entryAt(focused.focus);
4402
+ const result = entry
4403
+ ? await this.closeToolEntry(entry, input, origin)
4404
+ : this.unregisteredFocusClose(focused.focus) ?? { success: false, error: noLongerRegistered(focused.focus), isError: true };
4405
+ return focused.notClosed ? withSentence(result, focused.notClosed) : result;
4347
4406
  }
4407
+ const resolved = this.resolveToolChannelEntry(input.channelId, input.serverId, own);
4408
+ if (resolved.entry)
4409
+ return this.closeToolEntry(resolved.entry, input, origin);
4410
+ // The name, with its serverId or as the turn's own channel, means the
4411
+ // channel the all-except hold excepts, which its server no longer
4412
+ // registers (unregisteredFocusNamed). A module's close of such a name is
4413
+ // refused as any name no channel answers to.
4414
+ const gone = origin?.kind === 'module' ? undefined : this.unregisteredFocusNamed(input.channelId, input.serverId, own);
4415
+ return (gone && this.unregisteredFocusClose(gone)) || { success: false, error: resolved.error, isError: true };
4416
+ }
4417
+ /**
4418
+ * A tool close of the channel `entry`, which its name resolved to
4419
+ * (handleToolClose).
4420
+ */
4421
+ async closeToolEntry(entry, input, origin) {
4348
4422
  // From here, the channel's own id, whichever name the call gave for it
4349
4423
  // (a server-qualified one included).
4350
4424
  input = { ...input, channelId: entry.descriptor.id };
@@ -4375,20 +4449,23 @@ export class ChannelRegistry {
4375
4449
  // Closed already, perhaps only by default: the agent's close is still
4376
4450
  // its decision, and is recorded as one (setDesiredState), so a later
4377
4451
  // registration's lift of a default leaves the channel closed.
4378
- const ended = this.onToolClose?.(entry.serverId, input.channelId);
4452
+ const hook = this.toolCloseHook(refOf(entry));
4453
+ if ('error' in hook)
4454
+ return { success: false, error: hook.error, isError: true };
4455
+ const ended = hook.text;
4379
4456
  try {
4380
4457
  this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
4381
4458
  }
4382
4459
  catch (err) {
4383
4460
  return {
4384
4461
  success: false,
4385
- error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${err.message}${ended ? ` ${ended}` : ''}`,
4462
+ error: `Channel ${shown} is already closed, but recording the decision to close it failed: ${err.message}${ended ? ` ${ended()}` : ''}`,
4386
4463
  isError: true,
4387
4464
  };
4388
4465
  }
4389
4466
  return {
4390
4467
  success: true,
4391
- data: { channelId: input.channelId, serverId: entry.serverId, status: 'already closed' },
4468
+ data: { channelId: input.channelId, serverId: entry.serverId, status: 'already closed', ...(ended ? { note: ended() } : {}) },
4392
4469
  };
4393
4470
  }
4394
4471
  // Housekeeping must not override stated intent (issue #5: GC closes wore
@@ -4425,15 +4502,23 @@ export class ChannelRegistry {
4425
4502
  // A hold around this channel — the all-except scope it is the excepted
4426
4503
  // channel of — ends through the cancel flow: backlog released, end
4427
4504
  // recorded, never by overwriting state (#232 gap 9).
4428
- const ended = this.onToolClose?.(entry.serverId, input.channelId);
4505
+ const hook = this.toolCloseHook(refOf(entry));
4506
+ if ('error' in hook)
4507
+ return { success: false, error: hook.error, isError: true };
4508
+ const ended = hook.text;
4429
4509
  // A decision the log refused changed nothing for the channel, and the
4430
- // answer says so, and what the hook ended all the same.
4510
+ // answer says so, and what the hook ended all the same. Every answer
4511
+ // carries the hook's sentence, a success's as `note`: whether the close
4512
+ // went through says nothing of the focus, whose end may wait (an
4513
+ // operator's change to the store in progress). The sentence is read as
4514
+ // the answer is built, after the transport's round-trip, during which a
4515
+ // release that waited may have run.
4516
+ const withEnded = (text) => (ended ? `${text} ${ended()}` : text);
4431
4517
  try {
4432
4518
  this.setDesiredState(entry.serverId, input.channelId, 'closed', closeSource);
4433
4519
  }
4434
4520
  catch (err) {
4435
- const text = this.lifecycleFailureText('close', entry, unrecordedDecision(err));
4436
- return { success: false, error: ended ? `${text} ${ended}` : text, isError: true };
4521
+ return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, unrecordedDecision(err))), isError: true };
4437
4522
  }
4438
4523
  // This channel's typing, not every channel with its id (stopTyping's
4439
4524
  // fan-out of a bare name).
@@ -4441,7 +4526,7 @@ export class ChannelRegistry {
4441
4526
  // Refused now, the close stays recorded for the server's next
4442
4527
  // registration, and the text says so.
4443
4528
  if (!this.directsLifecycle(entry.serverId)) {
4444
- return { success: false, error: this.lifecycleFailureText('close', entry, this.lifecycleRefusal(entry.serverId)), isError: true };
4529
+ return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, this.lifecycleRefusal(entry.serverId))), isError: true };
4445
4530
  }
4446
4531
  try {
4447
4532
  const applied = await this.applyDesiredChannelState(entry.serverId, input.channelId);
@@ -4450,12 +4535,166 @@ export class ChannelRegistry {
4450
4535
  throw new ChannelLifecycleError(supersededClause('close', now), 'superseded');
4451
4536
  return {
4452
4537
  success: true,
4453
- data: { channelId: input.channelId, serverId: entry.serverId, status: 'closed' },
4538
+ data: { channelId: input.channelId, serverId: entry.serverId, status: 'closed', ...(ended ? { note: ended() } : {}) },
4454
4539
  };
4455
4540
  }
4456
4541
  catch (err) {
4457
- return { success: false, error: this.lifecycleFailureText('close', entry, err), isError: true };
4542
+ return { success: false, error: withEnded(this.lifecycleFailureText('close', entry, err)), isError: true };
4543
+ }
4544
+ }
4545
+ /**
4546
+ * The tool-close hook (onToolClose) for a channel, its throw taken as the
4547
+ * answer's text: a release of the hold that failed is the close's answer,
4548
+ * as the registry's other failures are, never a rejected call.
4549
+ */
4550
+ toolCloseHook(ref, opts) {
4551
+ try {
4552
+ return { text: this.onToolClose?.(ref.serverId, ref.channelId, opts) };
4553
+ }
4554
+ catch (err) {
4555
+ return { error: err instanceof Error ? err.message : String(err) };
4556
+ }
4557
+ }
4558
+ /**
4559
+ * A close naming the channel the all-except hold excepts, which its server
4560
+ * no longer registers: the close has nothing left to close, and the hold
4561
+ * nothing left to speak into, so it ends as a close of the registered
4562
+ * channel ends it, what it held released, and the answer says so, as a
4563
+ * failure, since no close ran. Undefined where no hold excepts the channel
4564
+ * (onToolClose answers nothing).
4565
+ */
4566
+ unregisteredFocusClose(gone) {
4567
+ const lead = noLongerRegistered(gone);
4568
+ const ended = this.toolCloseHook(gone, { unregistered: true });
4569
+ if ('error' in ended)
4570
+ return { success: false, error: `${lead} ${ended.error}`, isError: true };
4571
+ if (ended.text)
4572
+ return { success: false, error: `${lead} ${ended.text()}`, isError: true };
4573
+ return undefined;
4574
+ }
4575
+ /**
4576
+ * The channel the all-except hold excepts (focusChannelResolver), where a
4577
+ * channel_close names it by its id or its server-qualified name alone: no
4578
+ * serverId, and no channel of the calling turn's own answering to the name
4579
+ * (ownChannelNamed, which a turn's name means first). Such a name means
4580
+ * the hold's channel, registered or not, and not another server's channel
4581
+ * with the id, as when the hold's channel's server stopped registering it
4582
+ * and another server registered the id: that one is closed only by a close
4583
+ * that names its server. With it, where other registered channels answer
4584
+ * to the name, the sentence saying they weren't closed (`notClosed`).
4585
+ *
4586
+ * Where the name is the hold channel's and a channel of the calling turn's
4587
+ * own, another channel, answers to it too, the name means neither: a
4588
+ * resident repeating a close that failed before its focus ended, in a turn
4589
+ * that has since opened another server's channel with the id, would
4590
+ * otherwise close that channel and leave the focus in force. That close is
4591
+ * refused, naming both (`refusal`, focusOrOwnRefusal), and a close naming
4592
+ * either's server reaches it. A turn's name for the hold's channel itself,
4593
+ * and a name the turn's channels on more than one server answer to
4594
+ * (ownNeitherRefusal), resolve as any other.
4595
+ *
4596
+ * Undefined where no hold is in force, where the name isn't the hold
4597
+ * channel's, and where the hold's record can't be read, which leaves the
4598
+ * name to resolve as any other (resolveToolChannelEntry).
4599
+ *
4600
+ * Another channel the name reaches that has a tune-out of its own is
4601
+ * refused by a close until that tune-out is cancelled (tunedOutRefusal),
4602
+ * so its sentence says it is tuned out, and names the cancel first, then
4603
+ * the close by its id and server.
4604
+ */
4605
+ focusNamedBare(name, serverId, own) {
4606
+ if (serverId)
4607
+ return undefined;
4608
+ const mine = this.ownChannelNamed(name, own);
4609
+ if (mine && 'neither' in mine)
4610
+ return undefined;
4611
+ let focus;
4612
+ try {
4613
+ focus = this.focusChannelResolver?.();
4614
+ }
4615
+ catch {
4616
+ return undefined;
4617
+ }
4618
+ if (!focus || (name !== focus.channelId && name !== qualifiedChannelName(focus)))
4619
+ return undefined;
4620
+ if (mine)
4621
+ return sameChannel(mine.channel, focus) ? undefined : { refusal: this.focusOrOwnRefusal(name, focus, mine.channel, own) };
4622
+ const held = focus;
4623
+ const others = this.entriesNamed(name).filter((entry) => !sameChannel(refOf(entry), held));
4624
+ const tunedOut = others.filter((entry) => this.getDesiredState(entry.serverId, entry.descriptor.id) === 'tuned-out');
4625
+ const closable = others.filter((entry) => !tunedOut.includes(entry)).map(refOf);
4626
+ const sentences = [
4627
+ ...(closable.length > 0 ? [notClosedText(closable)] : []),
4628
+ ...tunedOut.map((entry) => {
4629
+ const ref = refOf(entry);
4630
+ return `Channel ${channelOnServer(ref)} was not closed: it is tuned out, and a close doesn't change that. ` +
4631
+ this.tuneOutEndText(entry, 'channel_close', `channelId ${quotedValue(ref.channelId)} and serverId ${quotedValue(ref.serverId)}`);
4632
+ }),
4633
+ ];
4634
+ return { focus: held, ...(sentences.length > 0 ? { notClosed: sentences.join(' ') } : {}) };
4635
+ }
4636
+ /**
4637
+ * The refusal of a channel_close's bare name (focusNamedBare) that both the
4638
+ * channel the all-except hold excepts (`focus`) and another channel of the
4639
+ * calling turn's own (`mine`, ownChannelNamed) answer to, each named by
4640
+ * what reaches it alone. The hold's channel is reached by its
4641
+ * server-qualified name, registered or not (focusNamedBare), unless a
4642
+ * channel of the turn's answers to that too; the turn's channel as a
4643
+ * refusal of the turn's names shows it (ownCandidates), unless another
4644
+ * channel of the turn's has its qualified name for its id. A serverId goes
4645
+ * with the channel's own id, so it's offered only where the name is the id
4646
+ * of both, for the hold's server (registered or not, the close reaches its
4647
+ * channel: unregisteredFocusNamed) and for the turn's channel's where that
4648
+ * server still registers it. Where the name is the hold channel's
4649
+ * server-qualified name, which another of the turn's channels has for its
4650
+ * id, the hold's channel is reached by its id and server.
4651
+ */
4652
+ focusOrOwnRefusal(name, focus, mine, own) {
4653
+ const qualified = qualifiedChannelName(focus);
4654
+ const focusReaches = this.ownChannelNamed(qualified, own) === undefined;
4655
+ const [theirs] = this.ownCandidates([mine]);
4656
+ // The turn's channel's name, as the focus's: a bare id answers to the
4657
+ // turn's own channels first, so one of them with that id catches it.
4658
+ const theirsReaches = theirs.reaches && this.ownChannelNamed(theirs.text, own) === undefined;
4659
+ const parts = [
4660
+ { text: focusReaches ? quotedValue(qualified) : channelOnServer(focus), reaches: focusReaches },
4661
+ { text: theirsReaches ? quotedValue(theirs.text) : theirs.reaches ? channelOnServer(mine) : theirs.text, reaches: theirsReaches },
4662
+ ];
4663
+ const named = parts.filter((p) => p.reaches).map((p) => p.text);
4664
+ const byName = named.length === 2 ? 'channelId set to one of them' : named.length === 1 ? `channelId ${named[0]}` : '';
4665
+ const servers = name === focus.channelId ? [focus.serverId, ...(this.entryAt(mine) ? [mine.serverId] : [])] : [];
4666
+ const byServer = servers.length > 0 ? `serverId set to ${servers.map((id) => JSON.stringify(id)).join(' or ')}` : '';
4667
+ const how = [byName, byServer].filter((h) => h !== '').join(', or with ');
4668
+ const focusRoute = name !== focus.channelId && !focusReaches
4669
+ ? ` For the channel the focus excepts, call again with channelId ${quotedValue(focus.channelId)} and serverId ${JSON.stringify(focus.serverId)}.`
4670
+ : '';
4671
+ return `Channel ${JSON.stringify(name)} names both the channel the all-except focus excepts (${parts[0].text}) ` +
4672
+ `and this turn's channel (${parts[1].text}), so nothing ran.${how ? ` Call again with ${how}.` : ''}${focusRoute}`;
4673
+ }
4674
+ /**
4675
+ * The channel the all-except hold excepts (focusChannelResolver), where a
4676
+ * channel tool's name with a serverId, or the calling turn's own channel's
4677
+ * name, means it though its server no longer registers it: with a
4678
+ * serverId, that server's channel with the id; inside the calling turn,
4679
+ * the turn's own channel the name answers to by its id (ownChannelNamed).
4680
+ * A name given alone is focusNamedBare's. Undefined where no hold is in
4681
+ * force, where its channel is still registered, and where the hold's
4682
+ * record can't be read, which leaves the name's refusal as it was.
4683
+ */
4684
+ unregisteredFocusNamed(name, serverId, own) {
4685
+ let focus;
4686
+ try {
4687
+ focus = this.focusChannelResolver?.();
4458
4688
  }
4689
+ catch {
4690
+ return undefined;
4691
+ }
4692
+ if (!focus || this.entryAt(focus))
4693
+ return undefined;
4694
+ if (serverId)
4695
+ return sameChannel(focus, { serverId, channelId: name }) ? focus : undefined;
4696
+ const mine = this.ownChannelNamed(name, own);
4697
+ return mine && 'channel' in mine && sameChannel(mine.channel, focus) ? focus : undefined;
4459
4698
  }
4460
4699
  async handleToolDecline(input, own) {
4461
4700
  const resolved = this.resolveToolChannelEntry(input.channelId, input.serverId, own);
@@ -4651,8 +4890,8 @@ export class ChannelRegistry {
4651
4890
  `diverted, and then call channel_close.${serverBack}`
4652
4891
  : `. If you still want it closed, call channel_close.${serverBack}`;
4653
4892
  closeError = decided === 'tuned-out'
4654
- ? `The decline is recorded, but the channel was tuned out meanwhile, and that decision stands: its messages go to ` +
4655
- `the subconscious${step}`
4893
+ ? `The decline is recorded, but the channel was tuned out meanwhile, and that decision stands: its messages ` +
4894
+ `${this.tuneOutHeld()}${step}`
4656
4895
  : `The decline is recorded, but a later decision to open the channel replaced it, and that decision stands${step}`;
4657
4896
  }
4658
4897
  else if (closed) {
@@ -5358,8 +5597,13 @@ export class ChannelRegistry {
5358
5597
  ? 'Nothing was posted: name the channel with channelId. A subagent has no default channel, since its plain '
5359
5598
  + 'speech is returned to whoever called it, so channel_publish sends only to a channel it names.'
5360
5599
  : caller !== undefined && inTurn && this.activeChannelResolver?.(caller) === null
5361
- ? `Nothing was posted: name the channel with channelId. This turn has no channel of its own, so its plain speech `
5362
- + `goes to none and channel_publish sends only to a channel it names. ${findIds}`
5600
+ ? (this.channellessTurn && !this.channellessTurn(caller)
5601
+ // The turn came from a channel, but its speech goes to none
5602
+ // (a locked turn held as drafts).
5603
+ ? `Nothing was posted: name the channel with channelId. Your plain speech goes to no channel now, so `
5604
+ + `channel_publish sends only to a channel it names. ${findIds}`
5605
+ : `Nothing was posted: name the channel with channelId. This turn has no channel of its own, so its plain speech `
5606
+ + `goes to none and channel_publish sends only to a channel it names. ${findIds}`)
5363
5607
  : `Nothing was posted: channel_publish needs a \`channelId\`, and there's no channel to default to yet. ${findIds}`,
5364
5608
  isError: true,
5365
5609
  };