@inhuman.tools/harness 0.2.0 → 0.3.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 (299) hide show
  1. package/compact.prompt +12 -2
  2. package/dist/builtin-tools/catalog-entry.d.ts.map +1 -1
  3. package/dist/builtin-tools/catalog-entry.js +1 -0
  4. package/dist/builtin-tools/catalog-entry.js.map +1 -1
  5. package/dist/builtin-tools/index.d.ts +1 -2
  6. package/dist/builtin-tools/index.d.ts.map +1 -1
  7. package/dist/builtin-tools/index.js +1 -2
  8. package/dist/builtin-tools/index.js.map +1 -1
  9. package/dist/builtin-tools/metadata/context-need.d.ts +1 -0
  10. package/dist/builtin-tools/metadata/context-need.d.ts.map +1 -1
  11. package/dist/builtin-tools/metadata/context-need.js +13 -6
  12. package/dist/builtin-tools/metadata/context-need.js.map +1 -1
  13. package/dist/builtin-tools/metadata/context-recall.d.ts +1 -5
  14. package/dist/builtin-tools/metadata/context-recall.d.ts.map +1 -1
  15. package/dist/builtin-tools/metadata/context-recall.js +10 -7
  16. package/dist/builtin-tools/metadata/context-recall.js.map +1 -1
  17. package/dist/builtin-tools/metadata/update-metadata.d.ts +1 -0
  18. package/dist/builtin-tools/metadata/update-metadata.d.ts.map +1 -1
  19. package/dist/builtin-tools/metadata/update-metadata.js +12 -12
  20. package/dist/builtin-tools/metadata/update-metadata.js.map +1 -1
  21. package/dist/builtin-tools/named-agents/agent-catalog.d.ts.map +1 -1
  22. package/dist/builtin-tools/named-agents/agent-catalog.js +9 -9
  23. package/dist/builtin-tools/named-agents/agent-catalog.js.map +1 -1
  24. package/dist/builtin-tools/named-agents/agent-stdlib.d.ts.map +1 -1
  25. package/dist/builtin-tools/named-agents/agent-stdlib.js +2 -0
  26. package/dist/builtin-tools/named-agents/agent-stdlib.js.map +1 -1
  27. package/dist/builtin-tools/schemas/tool-result.d.ts +2 -0
  28. package/dist/builtin-tools/schemas/tool-result.d.ts.map +1 -1
  29. package/dist/builtin-tools/schemas/tool-result.js +2 -0
  30. package/dist/builtin-tools/schemas/tool-result.js.map +1 -1
  31. package/dist/builtin-tools/shell/job-stop.d.ts +1 -0
  32. package/dist/builtin-tools/shell/job-stop.d.ts.map +1 -1
  33. package/dist/builtin-tools/shell/shell-tool.d.ts +1 -0
  34. package/dist/builtin-tools/shell/shell-tool.d.ts.map +1 -1
  35. package/dist/builtin-tools/toolkit/dist-tools.d.ts +1 -0
  36. package/dist/builtin-tools/toolkit/dist-tools.d.ts.map +1 -1
  37. package/dist/builtin-tools/toolkit/dist-tools.js +1 -0
  38. package/dist/builtin-tools/toolkit/dist-tools.js.map +1 -1
  39. package/dist/connections/adopt.d.ts +2 -0
  40. package/dist/connections/adopt.d.ts.map +1 -1
  41. package/dist/connections/adopt.js.map +1 -1
  42. package/dist/connections/index.d.ts +1 -1
  43. package/dist/connections/index.d.ts.map +1 -1
  44. package/dist/extensions/behavior.d.ts.map +1 -1
  45. package/dist/extensions/behavior.js +17 -4
  46. package/dist/extensions/behavior.js.map +1 -1
  47. package/dist/extensions/consent.d.ts +0 -68
  48. package/dist/extensions/consent.d.ts.map +1 -1
  49. package/dist/extensions/consent.js +0 -69
  50. package/dist/extensions/consent.js.map +1 -1
  51. package/dist/extensions/contributions.d.ts.map +1 -1
  52. package/dist/extensions/contributions.js +1 -0
  53. package/dist/extensions/contributions.js.map +1 -1
  54. package/dist/extensions/dispatch.d.ts +1 -9
  55. package/dist/extensions/dispatch.d.ts.map +1 -1
  56. package/dist/extensions/dispatch.js +5 -7
  57. package/dist/extensions/dispatch.js.map +1 -1
  58. package/dist/extensions/eval.d.ts.map +1 -1
  59. package/dist/extensions/eval.js +2 -0
  60. package/dist/extensions/eval.js.map +1 -1
  61. package/dist/extensions/hook-deadline.d.ts +28 -0
  62. package/dist/extensions/hook-deadline.d.ts.map +1 -0
  63. package/dist/extensions/hook-deadline.js +72 -0
  64. package/dist/extensions/hook-deadline.js.map +1 -0
  65. package/dist/extensions/host.d.ts +2 -0
  66. package/dist/extensions/host.d.ts.map +1 -1
  67. package/dist/extensions/host.js +15 -0
  68. package/dist/extensions/host.js.map +1 -1
  69. package/dist/extensions/inventory.d.ts +9 -0
  70. package/dist/extensions/inventory.d.ts.map +1 -1
  71. package/dist/extensions/inventory.js +15 -1
  72. package/dist/extensions/inventory.js.map +1 -1
  73. package/dist/extensions/notes.d.ts +1 -0
  74. package/dist/extensions/notes.d.ts.map +1 -1
  75. package/dist/extensions/notes.js +1 -0
  76. package/dist/extensions/notes.js.map +1 -1
  77. package/dist/extensions/schemas/bot-definition.d.ts +1 -1
  78. package/dist/extensions/schemas/bot-definition.d.ts.map +1 -1
  79. package/dist/extensions/schemas/plugin-manifest.d.ts +4 -4
  80. package/dist/extensions/schemas/plugin-manifest.d.ts.map +1 -1
  81. package/dist/extensions/schemas/plugin-manifest.js +13 -1
  82. package/dist/extensions/schemas/plugin-manifest.js.map +1 -1
  83. package/dist/extensions/state.d.ts.map +1 -1
  84. package/dist/extensions/state.js +5 -0
  85. package/dist/extensions/state.js.map +1 -1
  86. package/dist/extensions/value.d.ts +4 -0
  87. package/dist/extensions/value.d.ts.map +1 -1
  88. package/dist/extensions/value.js.map +1 -1
  89. package/dist/protocol/schemas/health.d.ts +1 -3
  90. package/dist/protocol/schemas/health.d.ts.map +1 -1
  91. package/dist/protocol/schemas/health.js.map +1 -1
  92. package/dist/protocol/schemas/identity.d.ts +0 -4
  93. package/dist/protocol/schemas/identity.d.ts.map +1 -1
  94. package/dist/protocol/schemas/identity.js.map +1 -1
  95. package/dist/protocol/schemas/loss.d.ts +0 -2
  96. package/dist/protocol/schemas/loss.d.ts.map +1 -1
  97. package/dist/protocol/schemas/loss.js.map +1 -1
  98. package/dist/protocol/schemas/message.d.ts +0 -1
  99. package/dist/protocol/schemas/message.d.ts.map +1 -1
  100. package/dist/protocol/supervision/loss.d.ts +0 -1
  101. package/dist/protocol/supervision/loss.d.ts.map +1 -1
  102. package/dist/protocol/supervision/loss.js +0 -3
  103. package/dist/protocol/supervision/loss.js.map +1 -1
  104. package/dist/providers/auth/index.d.ts +2 -2
  105. package/dist/providers/auth/index.d.ts.map +1 -1
  106. package/dist/providers/auth/index.js +2 -2
  107. package/dist/providers/auth/index.js.map +1 -1
  108. package/dist/providers/auth/loopback.d.ts +0 -2
  109. package/dist/providers/auth/loopback.d.ts.map +1 -1
  110. package/dist/providers/auth/loopback.js.map +1 -1
  111. package/dist/providers/chatgpt/connection.d.ts +1 -0
  112. package/dist/providers/chatgpt/connection.d.ts.map +1 -1
  113. package/dist/providers/chatgpt/connection.js +1 -1
  114. package/dist/providers/chatgpt/connection.js.map +1 -1
  115. package/dist/providers/grok/connection.d.ts +2 -1
  116. package/dist/providers/grok/connection.d.ts.map +1 -1
  117. package/dist/providers/grok/connection.js +1 -1
  118. package/dist/providers/grok/connection.js.map +1 -1
  119. package/dist/providers/mcp/fetch.d.ts.map +1 -1
  120. package/dist/providers/mcp/fetch.js +2 -6
  121. package/dist/providers/mcp/fetch.js.map +1 -1
  122. package/dist/providers/nous/driver.d.ts +1 -0
  123. package/dist/providers/nous/driver.d.ts.map +1 -1
  124. package/dist/providers/openai-compat/actor.d.ts.map +1 -1
  125. package/dist/providers/openai-compat/actor.js +167 -39
  126. package/dist/providers/openai-compat/actor.js.map +1 -1
  127. package/dist/providers/openai-compat/adopt.d.ts +2 -0
  128. package/dist/providers/openai-compat/adopt.d.ts.map +1 -1
  129. package/dist/providers/openai-compat/adopt.js.map +1 -1
  130. package/dist/providers/openai-compat/connection.d.ts +9 -2
  131. package/dist/providers/openai-compat/connection.d.ts.map +1 -1
  132. package/dist/providers/openai-compat/connection.js +35 -27
  133. package/dist/providers/openai-compat/connection.js.map +1 -1
  134. package/dist/providers/openai-compat/fetch-types.d.ts.map +1 -1
  135. package/dist/providers/openai-compat/fetch-types.js +2 -6
  136. package/dist/providers/openai-compat/fetch-types.js.map +1 -1
  137. package/dist/providers/openai-compat/index.d.ts +2 -2
  138. package/dist/providers/openai-compat/index.d.ts.map +1 -1
  139. package/dist/providers/openai-compat/index.js +2 -2
  140. package/dist/providers/openai-compat/index.js.map +1 -1
  141. package/dist/providers/openai-compat/model-capabilities.d.ts +7 -0
  142. package/dist/providers/openai-compat/model-capabilities.d.ts.map +1 -0
  143. package/dist/providers/openai-compat/model-capabilities.js +24 -0
  144. package/dist/providers/openai-compat/model-capabilities.js.map +1 -0
  145. package/dist/providers/openai-compat/schema.d.ts +1 -1
  146. package/dist/providers/web/registrations/ids.d.ts +0 -2
  147. package/dist/providers/web/registrations/ids.d.ts.map +1 -1
  148. package/dist/providers/web/registrations/ids.js +0 -1
  149. package/dist/providers/web/registrations/ids.js.map +1 -1
  150. package/dist/runtime/bot-mcp.d.ts.map +1 -1
  151. package/dist/runtime/bot-mcp.js +1 -4
  152. package/dist/runtime/bot-mcp.js.map +1 -1
  153. package/dist/runtime/intent-recovery.d.ts +3 -20
  154. package/dist/runtime/intent-recovery.d.ts.map +1 -1
  155. package/dist/runtime/intent-recovery.js +1 -122
  156. package/dist/runtime/intent-recovery.js.map +1 -1
  157. package/dist/server/ask-answer.d.ts.map +1 -1
  158. package/dist/server/ask-answer.js +3 -3
  159. package/dist/server/ask-answer.js.map +1 -1
  160. package/dist/server/daemon/kernel/KernelDaemon.d.ts +4 -0
  161. package/dist/server/daemon/kernel/KernelDaemon.d.ts.map +1 -1
  162. package/dist/server/daemon/kernel/KernelDaemon.js +100 -40
  163. package/dist/server/daemon/kernel/KernelDaemon.js.map +1 -1
  164. package/dist/server/daemon/kernel/completion-catalog.d.ts +8 -0
  165. package/dist/server/daemon/kernel/completion-catalog.d.ts.map +1 -1
  166. package/dist/server/daemon/kernel/completion-catalog.js +16 -0
  167. package/dist/server/daemon/kernel/completion-catalog.js.map +1 -1
  168. package/dist/server/daemon/kernel/fs-reflection/reflectKernel.d.ts.map +1 -1
  169. package/dist/server/daemon/kernel/fs-reflection/reflectKernel.js +47 -1
  170. package/dist/server/daemon/kernel/fs-reflection/reflectKernel.js.map +1 -1
  171. package/dist/server/daemon/kernel/glance-ask-line.d.ts +1 -6
  172. package/dist/server/daemon/kernel/glance-ask-line.d.ts.map +1 -1
  173. package/dist/server/daemon/kernel/glance-ask-line.js +1 -35
  174. package/dist/server/daemon/kernel/glance-ask-line.js.map +1 -1
  175. package/dist/server/daemon/kernel/settings-files.d.ts +1 -1
  176. package/dist/server/daemon/kernel/settings-files.d.ts.map +1 -1
  177. package/dist/server/daemon/kernel/settings-files.js +3 -6
  178. package/dist/server/daemon/kernel/settings-files.js.map +1 -1
  179. package/dist/server/daemon/project/ProjectDaemon.d.ts +4 -0
  180. package/dist/server/daemon/project/ProjectDaemon.d.ts.map +1 -1
  181. package/dist/server/daemon/project/ProjectDaemon.js +11 -0
  182. package/dist/server/daemon/project/ProjectDaemon.js.map +1 -1
  183. package/dist/server/daemon/session/SessionDaemon.d.ts +5 -3
  184. package/dist/server/daemon/session/SessionDaemon.d.ts.map +1 -1
  185. package/dist/server/daemon/session/SessionDaemon.js +54 -37
  186. package/dist/server/daemon/session/SessionDaemon.js.map +1 -1
  187. package/dist/server/daemon/session/SessionOrchestrator.d.ts +1 -0
  188. package/dist/server/daemon/session/SessionOrchestrator.d.ts.map +1 -1
  189. package/dist/server/daemon/session/SessionOrchestrator.js +1371 -1146
  190. package/dist/server/daemon/session/SessionOrchestrator.js.map +1 -1
  191. package/dist/server/daemon/session/agent-runtime.d.ts +2 -0
  192. package/dist/server/daemon/session/agent-runtime.d.ts.map +1 -1
  193. package/dist/server/daemon/session/agent-runtime.js +55 -6
  194. package/dist/server/daemon/session/agent-runtime.js.map +1 -1
  195. package/dist/server/daemon/session/ambient-compact.d.ts +3 -21
  196. package/dist/server/daemon/session/ambient-compact.d.ts.map +1 -1
  197. package/dist/server/daemon/session/ambient-compact.js +7 -116
  198. package/dist/server/daemon/session/ambient-compact.js.map +1 -1
  199. package/dist/server/daemon/session/behavior-host.d.ts +9 -3
  200. package/dist/server/daemon/session/behavior-host.d.ts.map +1 -1
  201. package/dist/server/daemon/session/behavior-host.js +222 -47
  202. package/dist/server/daemon/session/behavior-host.js.map +1 -1
  203. package/dist/server/daemon/session/compact.d.ts +0 -1
  204. package/dist/server/daemon/session/compact.d.ts.map +1 -1
  205. package/dist/server/daemon/session/compact.js +7 -1
  206. package/dist/server/daemon/session/compact.js.map +1 -1
  207. package/dist/server/daemon/session/plugin-abi.d.ts.map +1 -1
  208. package/dist/server/daemon/session/plugin-abi.js +7 -11
  209. package/dist/server/daemon/session/plugin-abi.js.map +1 -1
  210. package/dist/server/daemon/session/prompt-message-of.d.ts +1 -8
  211. package/dist/server/daemon/session/prompt-message-of.d.ts.map +1 -1
  212. package/dist/server/daemon/session/prompt-message-of.js +2 -12
  213. package/dist/server/daemon/session/prompt-message-of.js.map +1 -1
  214. package/dist/server/daemon/session/re-act-loop-intent-detected-trigger.d.ts +3 -42
  215. package/dist/server/daemon/session/re-act-loop-intent-detected-trigger.d.ts.map +1 -1
  216. package/dist/server/daemon/session/re-act-loop-intent-detected-trigger.js +4 -145
  217. package/dist/server/daemon/session/re-act-loop-intent-detected-trigger.js.map +1 -1
  218. package/dist/server/daemon/session/round-boundary.d.ts +13 -0
  219. package/dist/server/daemon/session/round-boundary.d.ts.map +1 -0
  220. package/dist/server/daemon/session/round-boundary.js +196 -0
  221. package/dist/server/daemon/session/round-boundary.js.map +1 -0
  222. package/dist/server/daemon/session/round-stop.d.ts +1 -25
  223. package/dist/server/daemon/session/round-stop.d.ts.map +1 -1
  224. package/dist/server/daemon/session/round-stop.js +1 -75
  225. package/dist/server/daemon/session/round-stop.js.map +1 -1
  226. package/dist/server/daemon/session/turn-iterator.d.ts +1 -44
  227. package/dist/server/daemon/session/turn-iterator.d.ts.map +1 -1
  228. package/dist/server/daemon/session/turn-iterator.js +1 -435
  229. package/dist/server/daemon/session/turn-iterator.js.map +1 -1
  230. package/dist/server/daemon/session/turn-resolvers.d.ts +1 -16
  231. package/dist/server/daemon/session/turn-resolvers.d.ts.map +1 -1
  232. package/dist/server/daemon/session/turn-resolvers.js +1 -28
  233. package/dist/server/daemon/session/turn-resolvers.js.map +1 -1
  234. package/dist/server/daemon/session/visual-store.d.ts +2 -2
  235. package/dist/server/daemon/session/visual-store.d.ts.map +1 -1
  236. package/dist/server/daemon/session/visual-store.js +19 -20
  237. package/dist/server/daemon/session/visual-store.js.map +1 -1
  238. package/dist/server/drivers/request-timeout.d.ts +1 -0
  239. package/dist/server/drivers/request-timeout.d.ts.map +1 -1
  240. package/dist/server/drivers/request-timeout.js +2 -1
  241. package/dist/server/drivers/request-timeout.js.map +1 -1
  242. package/dist/server/runtime-bin.js +5 -8
  243. package/dist/server/runtime-bin.js.map +1 -1
  244. package/dist/server/schemas/index.d.ts +0 -1
  245. package/dist/server/schemas/index.d.ts.map +1 -1
  246. package/dist/server/schemas/index.js +0 -1
  247. package/dist/server/schemas/index.js.map +1 -1
  248. package/dist/server/schemas/json-record.d.ts +0 -6
  249. package/dist/server/schemas/json-record.d.ts.map +1 -1
  250. package/dist/server/schemas/json-record.js +0 -16
  251. package/dist/server/schemas/json-record.js.map +1 -1
  252. package/dist/server/schemas/well-known.d.ts +0 -2
  253. package/dist/server/schemas/well-known.d.ts.map +1 -1
  254. package/dist/server/schemas/well-known.js +0 -1
  255. package/dist/server/schemas/well-known.js.map +1 -1
  256. package/dist/server/testkit/scripted-model.d.ts +11 -0
  257. package/dist/server/testkit/scripted-model.d.ts.map +1 -1
  258. package/dist/server/testkit/scripted-model.js +42 -3
  259. package/dist/server/testkit/scripted-model.js.map +1 -1
  260. package/dist/server/toolkit/actor.d.ts +0 -1
  261. package/dist/server/toolkit/actor.d.ts.map +1 -1
  262. package/dist/server/toolkit/actor.js +2 -5
  263. package/dist/server/toolkit/actor.js.map +1 -1
  264. package/dist/server/toolkit/catalog.d.ts +0 -2
  265. package/dist/server/toolkit/catalog.d.ts.map +1 -1
  266. package/dist/server/toolkit/catalog.js +0 -81
  267. package/dist/server/toolkit/catalog.js.map +1 -1
  268. package/dist/server/toolkit/pack-catalog.d.ts +2 -1
  269. package/dist/server/toolkit/pack-catalog.d.ts.map +1 -1
  270. package/dist/server/toolkit/pack-catalog.js +1 -0
  271. package/dist/server/toolkit/pack-catalog.js.map +1 -1
  272. package/dist/server/toolkit/tool-entry.d.ts.map +1 -1
  273. package/dist/server/toolkit/tool-entry.js +1 -0
  274. package/dist/server/toolkit/tool-entry.js.map +1 -1
  275. package/dist/server/toolkit/tool-table.d.ts +0 -1
  276. package/dist/server/toolkit/tool-table.d.ts.map +1 -1
  277. package/dist/server/toolkit/tool-table.js +0 -3
  278. package/dist/server/toolkit/tool-table.js.map +1 -1
  279. package/dist/server/util/read-field.d.ts +0 -2
  280. package/dist/server/util/read-field.d.ts.map +1 -1
  281. package/dist/server/util/read-field.js +0 -9
  282. package/dist/server/util/read-field.js.map +1 -1
  283. package/dist/server/util/tool-call-json.d.ts +0 -1
  284. package/dist/server/util/tool-call-json.d.ts.map +1 -1
  285. package/dist/server/util/tool-call-json.js +0 -25
  286. package/dist/server/util/tool-call-json.js.map +1 -1
  287. package/dist/session/index.d.ts +4 -1
  288. package/dist/session/index.d.ts.map +1 -1
  289. package/dist/session/index.js +10 -1
  290. package/dist/session/index.js.map +1 -1
  291. package/dist/tool-runtime/guard-tool.d.ts +3 -0
  292. package/dist/tool-runtime/guard-tool.d.ts.map +1 -1
  293. package/dist/tool-runtime/guard-tool.js +37 -3
  294. package/dist/tool-runtime/guard-tool.js.map +1 -1
  295. package/dist/yjs/document-service.d.ts +2 -4
  296. package/dist/yjs/document-service.d.ts.map +1 -1
  297. package/dist/yjs/document-service.js +0 -6
  298. package/dist/yjs/document-service.js.map +1 -1
  299. package/package.json +40 -40
@@ -1,36 +1,73 @@
1
+ var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) {
2
+ var useValue = arguments.length > 2;
3
+ for (var i = 0; i < initializers.length; i++) {
4
+ value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
5
+ }
6
+ return useValue ? value : void 0;
7
+ };
8
+ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
9
+ function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; }
10
+ var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
11
+ var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
12
+ var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
13
+ var _, done = false;
14
+ for (var i = decorators.length - 1; i >= 0; i--) {
15
+ var context = {};
16
+ for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
17
+ for (var p in contextIn.access) context.access[p] = contextIn.access[p];
18
+ context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); };
19
+ var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context);
20
+ if (kind === "accessor") {
21
+ if (result === void 0) continue;
22
+ if (result === null || typeof result !== "object") throw new TypeError("Object expected");
23
+ if (_ = accept(result.get)) descriptor.get = _;
24
+ if (_ = accept(result.set)) descriptor.set = _;
25
+ if (_ = accept(result.init)) initializers.unshift(_);
26
+ }
27
+ else if (_ = accept(result)) {
28
+ if (kind === "field") initializers.unshift(_);
29
+ else descriptor[key] = _;
30
+ }
31
+ }
32
+ if (target) Object.defineProperty(target, contextIn.name, descriptor);
33
+ done = true;
34
+ };
35
+ var __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) {
36
+ if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : "";
37
+ return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name });
38
+ };
1
39
  import { readFileSync } from "node:fs";
2
40
  import { EvalTrace } from "@here.build/arrival/provenance";
3
41
  import { docTag, fail, step, trace } from "@inhuman.tools/harness-diagnostics";
4
- import { AGENT_ASK, AGENT_SEND, AgentCallbackJob, AgentCallJob, BotCallJob, CallJob, AgentMessageJob, AgentNotifyJob, ApprovableJob, cancelOutcomeText, CompletionExpectation, CompletionJob, ContextNeedToolJob, delegatesPermissionMode, deskPermissionSnap, hostingTurnOf, McpToolExpectation, MCPToolJob, liveBackgroundJobs, permissionDeskSessionId, ProgrammaticJob, projectWorkerPermissions, SessionTurn, SystemToolJob, parseGrantRows, tapeCompletion, ToolCallJob, WebExtractToolJob, WebSearchToolJob, } from "@inhuman.tools/harness-model";
5
- import { RE_ACT_LOOP_INTENT_DETECTED_TRIGGER_STREAK_MAX, reActLoopIntentDetectedTriggerFor, } from "@inhuman.tools/harness-model/shared";
42
+ import { AGENT_ASK, AGENT_SEND, AgentCallbackJob, AgentCallJob, BotCallJob, CallJob, AgentMessageJob, AgentNotifyJob, ApprovableJob, AskAgentSpec, cancelOutcomeText, CompletionExpectation, CompletionJob, delegatesPermissionMode, deskPermissionSnap, hostingTurnOf, McpToolExpectation, MCPToolJob, liveBackgroundJobs, permissionDeskSessionId, ProgrammaticJob, projectWorkerPermissions, ReACTChunk, SessionTurn, SystemToolJob, parseGrantRows, tapeCompletion, ToolCallJob, WebExtractToolJob, WebSearchToolJob, } from "@inhuman.tools/harness-model";
43
+ import { reActLoopIntentDetectedTriggerFor } from "@inhuman.tools/harness-model/shared";
6
44
  import { connectionSupports, } from "@inhuman.tools/harness-provider-sdk";
7
45
  import { interpretSchemeRepl, liveModelsOf } from "@inhuman.tools/harness-scheme";
8
46
  import { walkWork, WorkExpectation } from "@inhuman.tools/plexus-expectations";
9
47
  import { ConnectionMiss } from "@inhuman.tools/plexus-expectations/driver";
10
48
  import { dropConnection, Orchestrator } from "@inhuman.tools/plexus-expectations/executor";
11
49
  import { closeTurn, isCallPaused, joinReleased, schemeDeskReadonly, shouldSkipTurnCancel, } from "@inhuman.tools/turn/agent-calls";
12
- import { autorun, comparer, reaction, runInAction, when } from "mobx";
50
+ import { createTurnResolvers, SessionOwnershipError } from "@inhuman.tools/turn/runtime";
51
+ import { autorun, comparer, computed, reaction, runInAction, when } from "mobx";
13
52
  import invariant from "tiny-invariant";
14
53
  import { admitPrivateMcpTool } from "../../../extensions/index.js";
54
+ import { pluginHookTimeoutMs } from "../../../extensions/hook-deadline.js";
15
55
  import { license, noteDenied } from "../../../policy-processor/license.js";
16
56
  import { scopes } from "../../../policy-processor/scope.js";
17
57
  import { documentKey } from "../../../protocol/document/index.js";
18
58
  import { hydrateMcpAlwaysAllows, MCP_HTTP_DRIVER, mcpSessionPersistAllows, mcpSessionPersistDenies, } from "../../../providers/mcp/index.js";
19
59
  import { borrowedEnsureKey, memoBorrowedEnsure, NOUS_GATEWAY_CONNECTION_ID, nousGatewaySourceOf, secretsConnectionId, } from "../../../providers/nous/index.js";
20
60
  import { isPrivatePluginConnection, parsePrivateConnectionKey, persistJobIntent } from "../../../runtime/index.js";
21
- import { abortParkableWork, dispatchedMcpSettled, failLeftoverApprovables, liveEvalWork, mcpJobOnWire, turnLoopStopped, } from "../../../session/index.js";
61
+ import { abortParkableWork, dispatchedMcpSettled, failLeftoverApprovables, liveEvalWork, isWorkReachable, mcpJobOnWire, turnLoopStopped, } from "../../../session/index.js";
22
62
  import { behaviorEntriesOf } from "../../../tool-catalog/index.js";
23
- import { agentCallFromWire, agentGuardedCall, distPolicySource, executeSystemTool, guardedCallOf, behaviorEntriesThatRun, guardHostTool, inboundTurnGuard, loadPolicySource, publishPolicyError, cancelUnstartedTool, stampLicenseDeny, stampUnentered, } from "../../../tool-runtime/index.js";
63
+ import { agentGuardedCall, distPolicySource, executeSystemTool, guardedCallOf, behaviorEntriesThatRun, guardHostTool, inboundTurnGuard, loadPolicySource, publishPolicyError, cancelUnstartedTool, stampLicenseDeny, stampUnentered, } from "../../../tool-runtime/index.js";
24
64
  import { completeCallExceptionally, deliverAgentMessage, executeAgentCall, executeBotCall, loadOpenCall, notifyAgentCall, sealCall, settleAgentCallback, } from "./agent-runtime.js";
25
65
  import { startAmbientCompact } from "./ambient-compact.js";
26
66
  import { isParkAbort, SessionBehavior } from "./behavior-host.js";
27
67
  import { loadCompactProgram } from "./compact.js";
28
68
  import { CompletionFlights } from "./completion-flight.js";
29
- import { pluginHookTimeoutMs } from "./hook-deadline.js";
30
69
  import { PROJECT_LAYER_POLL_MS, readProjectAuthorization } from "./project-authorization.js";
31
- import { internContinueStreak, internJudgeWire, runTextOnlyContinueIntern, } from "./re-act-loop-intent-detected-trigger.js";
32
- import { iterateTurn } from "./turn-iterator.js";
33
- import { createTurnResolvers } from "./turn-resolvers.js";
70
+ import { internJudgeWire, assessTextOnlyContinueIntern } from "./re-act-loop-intent-detected-trigger.js";
34
71
  import { resolveCompactStrategyPath } from "../../defaults/index.js";
35
72
  import { internedComplete } from "../../drivers/interned-complete.js";
36
73
  import { pickWebExtractConnection, pickWebSearchConnection } from "../../drivers/web-search/pick.js";
@@ -42,1238 +79,1426 @@ const OUTSIDE_WORKER_CEILING = "outside worker tool ceiling";
42
79
  function licenseDecisionNow(job) {
43
80
  return job.licenseDecision;
44
81
  }
45
- export class SessionOrchestrator extends Orchestrator {
46
- sessionDaemon;
47
- #disposeConnections;
48
- #disposeBoundWire;
49
- #disposeLive;
50
- #disposePromptQueue;
51
- #disposeAutoAllow;
52
- #disposeDesk;
53
- #disposeCompletions;
54
- #closed = false;
55
- /** Judge pairs that cannot send json_schema. Sticky skip, not retry. */
56
- #reActLoopIntentDetectedTriggerHaltKeys = new Set();
57
- /** Cutoff round → prefix key of the ambient completion still running. */
58
- #ambientInflight = new Map();
59
- #flights = new CompletionFlights((req) => this.#driveCompletion(req));
60
- /**
61
- * Rounds already open at {@link activate}. Recovery does not replay them
62
- * (`unknown-no-replay`); honor adopts one only after `startedAtMs`.
63
- */
64
- #completionBaseline = null;
65
- #loggedError = new Map();
66
- #loggedDriver = new Map();
67
- #gatewayEnsure = new Map();
68
- #behavior;
69
- /** One-shot recipe calls whose tools an agent guard narrowed, until the worker is minted. */
70
- #agentTools = new Map();
71
- get #behaviorNotes() {
72
- return this.sessionDaemon.kernel.behavior.notes;
73
- }
74
- constructor(sessionDaemon) {
75
- super(sessionDaemon.kernel.plexus, sessionDaemon.plexus);
76
- this.sessionDaemon = sessionDaemon;
77
- this.#behavior = new SessionBehavior(sessionDaemon.kernel.behavior, this.session.root, sessionDaemon.project.path, sessionDaemon.kernel.catalog, {
78
- syncTools: (turn) => sessionDaemon.syncBehaviorTools(turn),
79
- hookTimeoutMs: () => pluginHookTimeoutMs(this.session.root.kvs, this.kernel.root.kvs),
80
- });
81
- const scope = scopes.get(this.session.root);
82
- runInAction(() => {
83
- scope.cap = this.kernel.root.speculativePolicyProcessingTools;
84
- stampSessionPrograms(this.session.root, this.sessionDaemon.kernel.root, this.kernel.root);
85
- });
86
- scope.complete = (req) => this.complete(req);
87
- scope.judgeFallbackKvs = this.kernel.root.kvs;
88
- scope.judgeConfiguredModel = (connectionId) => this.#configuredModel(connectionId);
89
- scope.onPolicyError = (text) => publishPolicyError(this.session, text);
90
- scope.mcpPersistAllows = (job) => job instanceof MCPToolJob &&
91
- mcpSessionPersistAllows(job.connectionKey, job.serverToolName, this.session.root.toolAllowlist, this.session.root.toolBlocklist);
92
- scope.mcpPersistDenies = (job) => job instanceof MCPToolJob &&
93
- mcpSessionPersistDenies(job.connectionKey, job.serverToolName, this.session.root.toolBlocklist);
94
- runInAction(() => {
95
- hydrateMcpAlwaysAllows(this.session.root.toolAllowlist, this.kernel.root.connections);
96
- });
97
- // The authorization table: the user layer from `config.json`, the project layer from
98
- // `<repo>/.inhuman/authorization.json` (tighten only). Both are re-stamped when their files change, so a
99
- // mid-session tightening applies to the next call.
100
- const projectPath = this.sessionDaemon.project.path;
101
- const syncAuthorization = () => {
102
- const session = this.session.root;
103
- const user = this.kernel.root.authorizationJson;
104
- if (session.authorizationUserJson !== user)
105
- session.authorizationUserJson = user;
106
- const project = readProjectAuthorization(projectPath);
107
- if (session.authorizationProjectJson !== project)
108
- session.authorizationProjectJson = project;
109
- };
110
- runInAction(syncAuthorization);
111
- const poll = setInterval(() => {
112
- if (this.activated && !this.#closed)
113
- runInAction(syncAuthorization);
114
- }, PROJECT_LAYER_POLL_MS);
115
- poll.unref();
116
- const stopAuthorization = reaction(() => (this.activated ? this.kernel.root.authorizationJson : null), () => {
117
- if (!this.activated)
118
- return;
82
+ let SessionOrchestrator = (() => {
83
+ let _classSuper = Orchestrator;
84
+ let _instanceExtraInitializers = [];
85
+ let _private_get_callbackPending_decorators;
86
+ let _private_get_callbackPending_descriptor;
87
+ return class SessionOrchestrator extends _classSuper {
88
+ static {
89
+ const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
90
+ __esDecorate(this, _private_get_callbackPending_descriptor = { get: __setFunctionName(function () {
91
+ for (const job of this.session.root.motionWork()) {
92
+ if (job instanceof AgentCallbackJob && job.startedAtMs != null && !job.completed && !job.cancelled)
93
+ return true;
94
+ }
95
+ return false;
96
+ }, "#callbackPending", "get") }, _private_get_callbackPending_decorators, { kind: "getter", name: "#callbackPending", static: false, private: true, access: { has: obj => #callbackPending in obj, get: obj => obj.#callbackPending }, metadata: _metadata }, null, _instanceExtraInitializers);
97
+ if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
98
+ }
99
+ sessionDaemon = __runInitializers(this, _instanceExtraInitializers);
100
+ #disposeConnections;
101
+ #disposeBoundWire;
102
+ #disposeLive;
103
+ #disposePromptQueue;
104
+ #disposeAutoAllow;
105
+ #disposeDesk;
106
+ #disposeCompletions;
107
+ #closeScope;
108
+ #closed = false;
109
+ /** Judge pairs that cannot send json_schema. Sticky skip, not retry. */
110
+ #reActLoopIntentDetectedTriggerHaltKeys = new Set();
111
+ /** Cutoff round → prefix key of the ambient completion still running. */
112
+ #ambientInflight = new Map();
113
+ #flights = new CompletionFlights((req) => this.#driveCompletion(req));
114
+ /**
115
+ * Rounds already open at {@link activate}. Recovery does not replay them
116
+ * (`unknown-no-replay`); honor adopts one only after `startedAtMs`.
117
+ */
118
+ #completionBaseline = null;
119
+ #loggedError = new Map();
120
+ #loggedDriver = new Map();
121
+ #gatewayEnsure = new Map();
122
+ #behavior;
123
+ /** One-shot recipe calls whose tools an agent guard narrowed, until the worker is minted. */
124
+ #agentTools = new Map();
125
+ get #behaviorNotes() {
126
+ return this.sessionDaemon.kernel.behavior.notes;
127
+ }
128
+ constructor(sessionDaemon) {
129
+ super(sessionDaemon.kernel.plexus, sessionDaemon.plexus);
130
+ this.sessionDaemon = sessionDaemon;
131
+ this.#behavior = new SessionBehavior(sessionDaemon.kernel.behavior, this.session.root, sessionDaemon.project.path, sessionDaemon.kernel.catalog, {
132
+ syncTools: (turn) => sessionDaemon.syncBehaviorTools(turn),
133
+ hookTimeoutMs: () => pluginHookTimeoutMs(this.session.root.kvs, this.kernel.root.kvs),
134
+ canApplyResults: () => this.canApplyResults,
135
+ ownershipSignal: sessionDaemon.sessionRuntime.ownershipSignal,
136
+ });
137
+ const scope = scopes.get(this.session.root);
138
+ const ownershipSignal = sessionDaemon.sessionRuntime.ownershipSignal;
139
+ this.#closeScope = () => {
140
+ ownershipSignal.removeEventListener("abort", this.#closeScope);
141
+ scope.close();
142
+ };
143
+ ownershipSignal.addEventListener("abort", this.#closeScope, { once: true });
144
+ runInAction(() => {
145
+ scope.cap = this.kernel.root.speculativePolicyProcessingTools;
146
+ stampSessionPrograms(this.session.root, this.sessionDaemon.kernel.root, this.kernel.root);
147
+ });
148
+ scope.complete = (req) => this.complete(req);
149
+ scope.judgeFallbackKvs = this.kernel.root.kvs;
150
+ scope.judgeConfiguredModel = (connectionId) => this.#configuredModel(connectionId);
151
+ scope.onPolicyError = (text) => publishPolicyError(this.session, text);
152
+ scope.mcpPersistAllows = (job) => job instanceof MCPToolJob &&
153
+ mcpSessionPersistAllows(job.connectionKey, job.serverToolName, this.session.root.toolAllowlist, this.session.root.toolBlocklist);
154
+ scope.mcpPersistDenies = (job) => job instanceof MCPToolJob &&
155
+ mcpSessionPersistDenies(job.connectionKey, job.serverToolName, this.session.root.toolBlocklist);
156
+ runInAction(() => {
157
+ hydrateMcpAlwaysAllows(this.session.root.toolAllowlist, this.kernel.root.connections);
158
+ });
159
+ // The authorization table: the user layer from `config.json`, the project layer from
160
+ // `<repo>/.inhuman/authorization.json` (tighten only). Both are re-stamped when their files change, so a
161
+ // mid-session tightening applies to the next call.
162
+ const projectPath = this.sessionDaemon.project.path;
163
+ const syncAuthorization = () => {
164
+ if (!this.canApplyResults)
165
+ return;
166
+ const session = this.session.root;
167
+ const user = this.kernel.root.authorizationJson;
168
+ if (session.authorizationUserJson !== user)
169
+ session.authorizationUserJson = user;
170
+ const project = readProjectAuthorization(projectPath);
171
+ if (session.authorizationProjectJson !== project)
172
+ session.authorizationProjectJson = project;
173
+ };
119
174
  runInAction(syncAuthorization);
120
- });
121
- // Stored grants of project, bot and global scope come from the kernel's `grants.json`.
122
- scope.persistentGrants = () => parseGrantRows(this.kernel.root.grantsJson);
123
- scope.botName = () => {
124
- const spec = this.session.root.agent.spec;
125
- return spec.name.trim() === "" ? undefined : spec.name;
126
- };
127
- const stopGrants = reaction(() => this.kernel.root.grantsJson, () => scope.reconcileGrants());
128
- scope.onRunStopped = (stop) => {
129
- publishPolicyError(this.session, `Run stopped: ${stop.reason}`);
130
- };
131
- scope.onRunNotice = (notice) => {
132
- publishPolicyError(this.session, notice.text);
133
- };
134
- this.#disposeAutoAllow = () => {
135
- clearInterval(poll);
136
- stopAuthorization();
137
- stopGrants();
138
- };
139
- this.#disposeDesk = reaction(() => (this.activated ? this.#deskPermissionSnap() : null), (snap) => {
140
- if (snap == null)
141
- return;
142
- const desk = this.#permissionDesk();
143
- if (desk == null)
144
- return;
145
- runInAction(() => projectWorkerPermissions(this.session.root, desk));
146
- }, { equals: comparer.structural, fireImmediately: true });
147
- this.#disposePromptQueue = reaction(() => {
148
- if (!this.activated || this.#closed)
149
- return "";
150
- const root = this.session.root;
151
- if (root.findRunningTurn() != null)
152
- return "";
153
- const first = root.promptQueue[0];
154
- if (first == null)
155
- return "";
156
- return `${first.uuid}:${(root.kvs.connection ?? "").trim()}`;
157
- }, (key) => {
158
- if (key === "")
159
- return;
160
- void this.sessionDaemon.drainPromptQueue();
161
- });
162
- this.registerExecutor(SystemToolJob, (job) => this.#guardTool(job), async (job, signal) => {
163
- if (await this.#bodyRefused(job, signal))
164
- return;
165
- try {
166
- await executeSystemTool(job, signal, {
167
- shellTailBytes: this.kernel.root.shellTailBytes,
168
- stopJob: async (jobId) => {
169
- const nodes = this.session.root.motionWork();
170
- const id = jobId.trim();
171
- if (id === "")
172
- return { ok: true, value: { jobs: await liveBackgroundJobs(nodes) } };
173
- for (const node of nodes) {
174
- if (!(node instanceof ProgrammaticJob || node instanceof CallJob || node instanceof SystemToolJob))
175
- continue;
176
- if (!node.isTrackedBackground || node.toolCallId !== id)
177
- continue;
178
- if (node.completed)
179
- return { ok: true, value: { job_id: id, status: "completed" } };
180
- if (node.cancelled)
175
+ const poll = setInterval(() => {
176
+ if (this.activated && !this.#closed)
177
+ runInAction(syncAuthorization);
178
+ }, PROJECT_LAYER_POLL_MS);
179
+ poll.unref();
180
+ const stopAuthorization = reaction(() => (this.activated ? this.kernel.root.authorizationJson : null), () => {
181
+ if (!this.activated)
182
+ return;
183
+ runInAction(syncAuthorization);
184
+ });
185
+ // Stored grants of project, bot and global scope come from the kernel's `grants.json`.
186
+ scope.persistentGrants = () => parseGrantRows(this.kernel.root.grantsJson);
187
+ scope.botName = () => {
188
+ const spec = this.session.root.agent.spec;
189
+ return spec.name.trim() === "" ? undefined : spec.name;
190
+ };
191
+ const stopGrants = reaction(() => this.kernel.root.grantsJson, () => scope.reconcileGrants());
192
+ scope.onRunStopped = (stop) => {
193
+ publishPolicyError(this.session, `Run stopped: ${stop.reason}`);
194
+ };
195
+ scope.onRunNotice = (notice) => {
196
+ publishPolicyError(this.session, notice.text);
197
+ };
198
+ this.#disposeAutoAllow = () => {
199
+ clearInterval(poll);
200
+ stopAuthorization();
201
+ stopGrants();
202
+ };
203
+ this.#disposeDesk = reaction(() => (this.activated ? this.#deskPermissionSnap() : null), (snap) => {
204
+ if (snap == null || !this.canApplyResults)
205
+ return;
206
+ const desk = this.#permissionDesk();
207
+ if (desk == null)
208
+ return;
209
+ runInAction(() => projectWorkerPermissions(this.session.root, desk));
210
+ }, { equals: comparer.structural, fireImmediately: true });
211
+ this.#disposePromptQueue = reaction(() => {
212
+ if (!this.activated || this.#closed || !this.canApplyResults)
213
+ return "";
214
+ const root = this.session.root;
215
+ if (root.findRunningTurn() != null)
216
+ return "";
217
+ const first = root.promptQueue[0];
218
+ if (first == null)
219
+ return "";
220
+ return `${first.uuid}:${(root.kvs.connection ?? "").trim()}`;
221
+ }, (key) => {
222
+ if (key === "" || !this.canApplyResults)
223
+ return;
224
+ void this.sessionDaemon.drainPromptQueue();
225
+ });
226
+ this.registerExecutor(SystemToolJob, (job) => this.#guardTool(job), async (job, signal) => {
227
+ signal = this.#ownedSignal(signal);
228
+ if (await this.#bodyRefused(job, signal))
229
+ return;
230
+ try {
231
+ await executeSystemTool(job, signal, {
232
+ canApplyResults: () => this.canApplyResults,
233
+ shellTailBytes: this.kernel.root.shellTailBytes,
234
+ stopJob: async (jobId) => {
235
+ const nodes = this.session.root.motionWork();
236
+ const id = jobId.trim();
237
+ if (id === "")
238
+ return { ok: true, value: { jobs: await liveBackgroundJobs(nodes) } };
239
+ for (const node of nodes) {
240
+ if (!(node instanceof ProgrammaticJob || node instanceof CallJob || node instanceof SystemToolJob))
241
+ continue;
242
+ if (!node.isTrackedBackground || node.toolCallId !== id)
243
+ continue;
244
+ if (node.completed)
245
+ return { ok: true, value: { job_id: id, status: "completed" } };
246
+ if (node.cancelled)
247
+ return { ok: true, value: { job_id: id, status: "cancelled" } };
248
+ this.#abortWork(node);
181
249
  return { ok: true, value: { job_id: id, status: "cancelled" } };
182
- this.#abortWork(node);
183
- return { ok: true, value: { job_id: id, status: "cancelled" } };
184
- }
185
- return { ok: true, value: { job_id: id, status: "absent" } };
186
- },
187
- });
188
- }
189
- finally {
190
- await this.#behavior.finishTool(job, signal);
191
- if (!job.completed && !job.cancelled)
192
- job.markComplete();
193
- }
194
- }, { seal: false });
195
- this.registerExecutor(MCPToolJob, (job) => this.#guardTool(job), async (job, signal) => {
196
- if (await this.#bodyRefused(job, signal))
197
- return;
198
- try {
199
- await this.#runMcp(job, signal);
200
- }
201
- finally {
202
- await this.#behavior.finishTool(job, signal);
203
- }
204
- }, { seal: false });
205
- this.registerExecutor(WebSearchToolJob, (job) => this.#guardTool(job), (job, signal) => this.#runWeb(job, signal), { seal: false });
206
- this.registerExecutor(WebExtractToolJob, (job) => this.#guardTool(job), (job, signal) => this.#runWeb(job, signal), { seal: false });
207
- this.registerExecutor(BotCallJob, (job) => this.#guardTool(job), async (job, signal) => {
208
- try {
209
- await executeBotCall(job, this.#agentHost());
210
- }
211
- catch (error) {
212
- sealCall(job, "error", error instanceof Error ? error.message : String(error));
213
- }
214
- finally {
215
- await waitUntil(() => job.completed, signal);
216
- await this.#finishCall(job, signal);
217
- }
218
- }, { seal: false });
219
- this.registerExecutor(AgentCallJob, (job) => this.#guardTool(job), async (job, signal) => {
220
- try {
221
- await executeAgentCall(job, this.#agentHost(), this.#agentTools.get(job));
222
- }
223
- catch (error) {
224
- sealCall(job, "error", error instanceof Error ? error.message : String(error));
225
- }
226
- finally {
227
- await waitUntil(() => job.completed, signal);
228
- await this.#finishCall(job, signal);
229
- }
230
- }, { seal: false });
231
- this.registerExecutor(AgentCallbackJob, () => true, async (job, signal) => {
232
- if (await this.#bodyRefused(job, signal))
233
- return;
234
- await settleAgentCallback(job, this.#agentHost(), (node) => this.#abortWork(node));
235
- });
236
- this.registerExecutor(AgentNotifyJob, () => true, async (job, signal) => {
237
- if (await this.#bodyRefused(job, signal))
238
- return;
239
- notifyAgentCall(job, this.#agentHost());
240
- });
241
- this.registerExecutor(AgentMessageJob, (job) => this.#license(job), async (job, signal) => {
242
- if (await this.#bodyRefused(job, signal))
243
- return;
244
- await deliverAgentMessage(job, this.#agentHost(), signal);
245
- });
246
- this.registerExecutor(ContextNeedToolJob, (job) => this.#license(job), async (job) => {
247
- job.outcomeSnap ??= { ok: true };
248
- });
249
- this.registerExecutor(ProgrammaticJob,
250
- // Bag is not remainder. Inner System/MCP/agent leaves still `#license`.
251
- () => true, async (job, signal) => {
252
- if (!(await this.#waitEvalAdmission(signal)))
253
- return;
254
- const catalog = sessionCatalog(this.session.root) ?? new KernelToolCatalog();
255
- await interpretSchemeRepl(job, {
256
- catalog,
257
- root: this.session.root.path.trim() === "" ? process.cwd() : this.session.root.path,
258
- signal,
259
- sessionId: this.session.root.sessionId,
260
- observe: (run) => {
261
- const evalTrace = new EvalTrace();
262
- evalTrace.withAutoBindings();
263
- return run(evalTrace);
264
- },
265
- readonly: schemeDeskReadonly(this.session.root, enclosingCallOf(this.sessionDaemon.kernel, this.session.root, job)),
266
- session: this.session.root,
267
- doors: this.sessionDaemon.doors,
268
- evalProgram: (hostingTurnOf(this.session.root, job) ?? this.session.root.head)?.evalSpec != null,
269
- connectionId: this.session.root.kvs.connection?.trim() || undefined,
270
- completionModel: this.session.root.kvs.model?.trim() || undefined,
271
- complete: async (req) => {
272
- try {
273
- return await this.complete(req);
250
+ }
251
+ return { ok: true, value: { job_id: id, status: "absent" } };
252
+ },
253
+ });
254
+ }
255
+ finally {
256
+ if (this.canApplyResults) {
257
+ await this.#behavior.finishTool(job, signal);
258
+ if (this.canApplyResults && !job.completed && !job.cancelled)
259
+ job.markComplete();
274
260
  }
275
- catch (error) {
276
- if (error instanceof ConnectionMiss)
277
- throw new Error(error.message);
278
- throw error;
261
+ }
262
+ }, { seal: false });
263
+ this.registerExecutor(MCPToolJob, (job) => this.#guardTool(job), async (job, signal) => {
264
+ if (await this.#bodyRefused(job, signal))
265
+ return;
266
+ try {
267
+ await this.#runMcp(job, signal);
268
+ }
269
+ finally {
270
+ if (this.canApplyResults)
271
+ await this.#behavior.finishTool(job, signal);
272
+ }
273
+ }, { seal: false });
274
+ this.registerExecutor(WebSearchToolJob, (job) => this.#guardTool(job), (job, signal) => this.#runWeb(job, signal), { seal: false });
275
+ this.registerExecutor(WebExtractToolJob, (job) => this.#guardTool(job), (job, signal) => this.#runWeb(job, signal), { seal: false });
276
+ this.registerExecutor(BotCallJob, (job) => this.#guardTool(job), async (job, signal) => {
277
+ try {
278
+ await executeBotCall(job, this.#agentHost());
279
+ }
280
+ catch (error) {
281
+ if (this.canApplyResults)
282
+ sealCall(job, "error", error instanceof Error ? error.message : String(error));
283
+ }
284
+ finally {
285
+ if (this.canApplyResults) {
286
+ await waitUntil(() => job.completed, signal);
287
+ if (this.canApplyResults)
288
+ await this.#finishCall(job, signal);
279
289
  }
280
- },
281
- connectionModels: (name) => {
282
- const spec = this.sessionDaemon.kernel.plexus.root.connections[name];
283
- invariant(spec != null, `unknown connection ${name}`);
284
- return liveModelsOf(spec);
285
- },
286
- admit: (call) => this.sessionDaemon.project.toolCalls.admit(call, catalog, {
287
- session: this.session.root,
288
- loadAgentCall: (uuid) => loadOpenCall(this.sessionDaemon.kernel, this.session.root, uuid),
289
- }),
290
- abort: (leaf, reason) => this.#abortLeaf(leaf, reason),
291
- notify: (note) => {
292
- const call = enclosingCallOf(this.sessionDaemon.kernel, this.session.root, job);
293
- if (call != null) {
294
- call.notifications.push(note);
295
- return;
290
+ }
291
+ }, { seal: false });
292
+ this.registerExecutor(AgentCallJob, (job) => this.#guardTool(job), async (job, signal) => {
293
+ try {
294
+ await executeAgentCall(job, this.#agentHost(), this.#agentTools.get(job));
295
+ }
296
+ catch (error) {
297
+ if (this.canApplyResults)
298
+ sealCall(job, "error", error instanceof Error ? error.message : String(error));
299
+ }
300
+ finally {
301
+ if (this.canApplyResults) {
302
+ await waitUntil(() => job.completed, signal);
303
+ if (this.canApplyResults)
304
+ await this.#finishCall(job, signal);
296
305
  }
297
- job.notifications.push(note);
298
- },
306
+ }
307
+ }, { seal: false });
308
+ this.registerExecutor(AgentCallbackJob, () => true, async (job, signal) => {
309
+ if (await this.#bodyRefused(job, signal))
310
+ return;
311
+ await settleAgentCallback(job, this.#agentHost(), (node) => this.#abortWork(node));
299
312
  });
300
- }, { seal: false });
301
- this.registerExecutor(SessionTurn, (turn) => inboundTurnGuard(turn), async (turn, signal) => {
302
- // Any other abort runs the close below; the iterator stops at once.
303
- if (!(await this.#waitEvalAdmission(signal)) && isParkAbort(signal))
304
- return;
305
- if (turn.reducerExited)
306
- return;
307
- // Rewound away before it ran. Its close touches only itself: the steers and holds belong to the live head.
308
- if (!this.reachableJobs.includes(turn)) {
309
- closeTurn(turn, { kind: "rewound" }, this.#closeOptions());
310
- return;
311
- }
312
- const session = this.session.root;
313
- const toolCalls = this.sessionDaemon.project.toolCalls;
314
- let broke = "";
315
- try {
316
- const connectionId = session.kvs.connection?.trim() ?? "";
317
- const model = session.kvs.model?.trim() ?? "";
318
- trace("harness:turn", `open ${turn.uuid} connection=${connectionId || "(none)"} model=${model || "(none)"}`);
319
- await iterateTurn(turn, {
320
- session,
321
- resolvers: createTurnResolvers({
322
- complete: (req) => this.complete(req),
323
- join: (job, abort) => waitCompleted(job, abort, this.sessionDaemon.kernel.catalog),
324
- compactHost: () => this.#compactHost(turn, session, connectionId, model),
325
- }),
326
- notes: this.#behaviorNotes,
327
- compactKeepTurns: this.kernel.root.compactKeepTurns,
328
- hardCompactTrigger: this.kernel.root.compactTriggers.hard,
329
- loopLimits: () => {
330
- const root = this.kernel.root;
331
- // A thread reached through bot/ask has its own round cap.
332
- const bot = session.agent.spec.botDepth > 0;
333
- return {
334
- maxRounds: bot ? root.maxBotRounds : root.maxRounds,
335
- lengthContinuations: root.lengthContinuations,
336
- };
313
+ this.registerExecutor(AgentNotifyJob, () => true, async (job, signal) => {
314
+ if (await this.#bodyRefused(job, signal))
315
+ return;
316
+ notifyAgentCall(job, this.#agentHost());
317
+ });
318
+ this.registerExecutor(AgentMessageJob, (job) => this.#license(job), async (job, signal) => {
319
+ if (await this.#bodyRefused(job, signal))
320
+ return;
321
+ await deliverAgentMessage(job, this.#agentHost(), signal);
322
+ });
323
+ this.registerExecutor(ProgrammaticJob,
324
+ // Bag is not remainder. Inner System/MCP/agent leaves still `#license`.
325
+ () => true, async (job, signal) => {
326
+ signal = this.#ownedSignal(signal);
327
+ if (!(await this.#waitEvalAdmission(signal)))
328
+ return;
329
+ if (!this.canApplyResults)
330
+ return;
331
+ const catalog = sessionCatalog(this.session.root) ?? new KernelToolCatalog();
332
+ await interpretSchemeRepl(job, {
333
+ canApplyResults: () => this.canApplyResults,
334
+ catalog,
335
+ root: this.session.root.path.trim() === "" ? process.cwd() : this.session.root.path,
336
+ signal,
337
+ sessionId: this.session.root.sessionId,
338
+ observe: (run) => {
339
+ const evalTrace = new EvalTrace();
340
+ evalTrace.withAutoBindings();
341
+ return run(evalTrace);
337
342
  },
338
- remoteMcp: (key) => this.kernel.root.connections[key]?.driver?.name === MCP_HTTP_DRIVER,
339
- admit: (call) => {
340
- const catalog = sessionCatalog(session) ?? toolCalls.kernel.catalog;
341
- const ctx = {
342
- session,
343
- originTurn: turn,
344
- parent: turn,
345
- loadAgentCall: (uuid) => loadOpenCall(this.sessionDaemon.kernel, session, uuid),
346
- };
347
- return call.function.name === AGENT_ASK || call.function.name === AGENT_SEND
348
- ? agentCallFromWire(call, catalog, ctx)
349
- : toolCalls.admit(call, catalog, ctx);
343
+ readonly: schemeDeskReadonly(this.session.root, enclosingCallOf(this.sessionDaemon.kernel, this.session.root, job)),
344
+ session: this.session.root,
345
+ doors: this.sessionDaemon.doors,
346
+ evalProgram: (hostingTurnOf(this.session.root, job) ?? this.session.root.head)?.evalSpec != null,
347
+ connectionId: this.session.root.kvs.connection?.trim() || undefined,
348
+ completionModel: this.session.root.kvs.model?.trim() || undefined,
349
+ complete: async (req) => {
350
+ try {
351
+ return await this.complete(req);
352
+ }
353
+ catch (error) {
354
+ if (error instanceof ConnectionMiss)
355
+ throw new TypeError(error.message);
356
+ throw error;
357
+ }
350
358
  },
351
- waitInbound: (abort) => this.#inboundResumed(turn, abort, () => turnLoopStopped(turn, abort)),
352
- openRound: async (resumed) => {
353
- const behavior = await this.#behavior.enterTurn(turn, resumed);
354
- return (behavior != null && !turnLoopStopped(turn, signal) && (await this.#behavior.openRound(turn, behavior)));
359
+ connectionModels: (name) => {
360
+ const spec = this.sessionDaemon.kernel.plexus.root.connections[name];
361
+ invariant(spec != null, `unknown connection ${name}`);
362
+ return liveModelsOf(spec);
355
363
  },
356
- closeRound: (settle) => this.#behavior.closeRound(turn, settle),
357
- classifyIntent: (pew, abort) => this.#reActLoopIntentDetectedTrigger(turn, pew, abort),
358
- startAmbientCompact: (options) => {
359
- void startAmbientCompact({
360
- ...options,
361
- complete: (req) => this.complete(req),
362
- inflight: this.#ambientInflight,
364
+ admit: (call) => {
365
+ if (!this.canApplyResults)
366
+ throw new SessionOwnershipError();
367
+ return this.sessionDaemon.project.toolCalls.admit(call, catalog, {
368
+ session: this.session.root,
369
+ loadAgentCall: (uuid) => loadOpenCall(this.sessionDaemon.kernel, this.session.root, uuid),
363
370
  });
364
371
  },
365
- }, signal);
366
- }
367
- catch (error) {
368
- broke = error instanceof Error ? error.message : String(error);
369
- fail("turn", error);
370
- }
371
- finally {
372
- trace("harness:turn", `close ${turn.uuid} cancel=${turn.cancelRequested} park=${signal.reason === "park"}`);
373
- if (signal.reason === "park")
374
- this.#behavior.dropTurn(turn);
375
- else {
376
- // The post hooks are bounded, but a throw must still reach the close below.
377
- try {
378
- await this.#behavior.leaveTurn(turn, broke);
372
+ abort: (leaf, reason) => this.#abortLeaf(leaf, reason),
373
+ notify: (note) => {
374
+ if (!this.canApplyResults)
375
+ return;
376
+ const call = enclosingCallOf(this.sessionDaemon.kernel, this.session.root, job);
377
+ if (call != null) {
378
+ call.notifications.push(note);
379
+ return;
380
+ }
381
+ job.notifications.push(note);
382
+ },
383
+ });
384
+ }, { seal: false });
385
+ this.registerExecutor(SessionTurn, (turn) => inboundTurnGuard(turn), async (turn, signal) => {
386
+ if (!this.canApplyResults)
387
+ return;
388
+ signal = AbortSignal.any([signal, this.sessionDaemon.sessionRuntime.signal]);
389
+ // Any other abort runs the close below; the iterator stops at once.
390
+ if (!(await this.#waitEvalAdmission(signal)) && isParkAbort(signal))
391
+ return;
392
+ if (!this.canApplyResults)
393
+ return;
394
+ if (turn.reducerExited)
395
+ return;
396
+ // Rewound away before it ran. Its close touches only itself: the steers and holds belong to the live head.
397
+ if (!this.reachableJobs.includes(turn)) {
398
+ closeTurn(turn, { kind: "rewound" }, this.#closeOptions());
399
+ return;
400
+ }
401
+ const session = this.session.root;
402
+ const toolCalls = this.sessionDaemon.project.toolCalls;
403
+ let broke = "";
404
+ try {
405
+ const connectionId = session.kvs.connection?.trim() ?? "";
406
+ const model = session.kvs.model?.trim() ?? "";
407
+ trace("harness:turn", `open ${turn.uuid} connection=${connectionId || "(none)"} model=${model || "(none)"}`);
408
+ await this.sessionDaemon.sessionRuntime.run(turn, {
409
+ session,
410
+ resolvers: createTurnResolvers({
411
+ complete: (req) => this.complete(req),
412
+ join: (job, abort) => waitCompleted(job, abort, this.sessionDaemon.kernel.catalog),
413
+ compactHost: () => this.#compactHost(turn, session, connectionId, model),
414
+ }),
415
+ notes: this.#behaviorNotes,
416
+ diagnostics: { step, trace, fail },
417
+ compactKeepTurns: this.kernel.root.compactKeepTurns,
418
+ hardCompactTrigger: this.kernel.root.compactTriggers.hard,
419
+ loopLimits: () => {
420
+ const root = this.kernel.root;
421
+ // A thread reached through bot/ask has its own round cap.
422
+ const bot = session.agent.spec.botDepth > 0;
423
+ return {
424
+ maxRounds: bot ? root.maxBotRounds : root.maxRounds,
425
+ lengthContinuations: root.lengthContinuations,
426
+ };
427
+ },
428
+ remoteMcp: (key) => this.kernel.root.connections[key]?.driver?.name === MCP_HTTP_DRIVER,
429
+ admit: (call) => {
430
+ if (!this.canApplyResults)
431
+ throw new SessionOwnershipError();
432
+ const catalog = sessionCatalog(session) ?? toolCalls.kernel.catalog;
433
+ const ctx = {
434
+ session,
435
+ originTurn: turn,
436
+ parent: turn,
437
+ loadAgentCall: (uuid) => loadOpenCall(this.sessionDaemon.kernel, session, uuid),
438
+ };
439
+ return toolCalls.admit(call, catalog, ctx);
440
+ },
441
+ waitInbound: (abort) => this.#inboundResumed(turn, abort, () => turnLoopStopped(turn, abort)),
442
+ openRound: async (resumed) => {
443
+ const behavior = await this.#behavior.enterTurn(turn, resumed, signal);
444
+ return (behavior != null && !turnLoopStopped(turn, signal) && (await this.#behavior.openRound(turn, behavior)));
445
+ },
446
+ closeRound: (settle) => this.#behavior.closeRound(turn, settle),
447
+ classifyIntent: (pew, abort) => this.#reActLoopIntentDetectedTrigger(turn, pew, abort),
448
+ startAmbientCompact: (options) => {
449
+ void startAmbientCompact({
450
+ ...options,
451
+ complete: (req) => this.complete(req),
452
+ inflight: this.#ambientInflight,
453
+ });
454
+ },
455
+ }, signal);
456
+ }
457
+ catch (error) {
458
+ if (!this.canApplyResults)
459
+ return;
460
+ broke = error instanceof Error ? error.message : String(error);
461
+ fail("turn", error);
462
+ }
463
+ finally {
464
+ if (!this.canApplyResults || signal.reason === "park")
465
+ this.#behavior.dropTurn(turn);
466
+ else {
467
+ trace("harness:turn", `close ${turn.uuid} cancel=${turn.cancelRequested} park=false`);
468
+ // The post hooks are bounded, but a throw must still reach the close below.
469
+ try {
470
+ await this.#behavior.leaveTurn(turn, broke || turn.broke);
471
+ }
472
+ catch (error) {
473
+ if (this.canApplyResults)
474
+ fail("turn boundary", error);
475
+ }
476
+ if (this.canApplyResults) {
477
+ const ending = turn.cancelRequested
478
+ ? { kind: "cancel" }
479
+ : (broke || turn.broke) === ""
480
+ ? { kind: "finish" }
481
+ : { kind: "broke", detail: broke || turn.broke };
482
+ closeTurn(turn, ending, this.#closeOptions());
483
+ this.#releaseUnansweredCall(turn);
484
+ // Self-service lane: the turn that was head takes the next queued message itself.
485
+ // The prompt-queue reaction stays as the fallback (a restart re-arms it).
486
+ void this.sessionDaemon.drainPromptQueue();
487
+ }
488
+ }
489
+ }
490
+ }, { seal: false });
491
+ this.#disposeLive = autorun(() => {
492
+ if (!this.activated || this.#closed || !this.canApplyResults)
493
+ return;
494
+ const reachable = this.reachableJobs;
495
+ for (const job of this.inFlightJobs()) {
496
+ if (!(job instanceof SessionTurn) || job.reducerExited || reachable.includes(job))
497
+ continue;
498
+ failLeftoverApprovables(job, (leaf) => this.#abortLeaf(leaf), this.sessionDaemon.kernel.catalog);
499
+ }
500
+ });
501
+ // Honor refuses an unstamped completion (`shouldHonor`). An executor would
502
+ // stamp `orchestrationResult` or `completed` on every historical round.
503
+ // Snapshot at activate is the recovery set; only a round that appears after
504
+ // that is a new attempt, including an isolated `.prompt`.
505
+ const stopBaseline = reaction(() => this.activated, (activated) => {
506
+ if (!activated || this.#completionBaseline !== null)
507
+ return;
508
+ this.#completionBaseline = new Set(this.#openUnstartedCompletions());
509
+ });
510
+ const stopDrive = reaction(() => this.#openUnstartedCompletions(), (jobs) => {
511
+ const baseline = this.#completionBaseline;
512
+ if (baseline === null)
513
+ return;
514
+ for (const job of jobs) {
515
+ if (baseline.has(job))
516
+ continue;
517
+ baseline.add(job);
518
+ const pew = job.expectation;
519
+ void this.complete(completionRequest(pew)).catch((error) => {
520
+ if (!this.canApplyResults || pew.isDone || this.#flights.aborted(pew))
521
+ return;
522
+ pew.fail("crash", error instanceof Error ? error.message : String(error), "null");
523
+ });
524
+ }
525
+ }, { equals: comparer.shallow });
526
+ this.#disposeCompletions = () => {
527
+ stopBaseline();
528
+ stopDrive();
529
+ };
530
+ this.#disposeConnections = autorun(() => {
531
+ if (!this.activated || this.#closed)
532
+ return;
533
+ for (const name of Object.keys(this.drivers)) {
534
+ const spec = this.drivers[name];
535
+ if (spec === undefined)
536
+ continue;
537
+ if (!this.#loggedDriver.has(name)) {
538
+ this.#loggedDriver.set(name, "");
539
+ step(`driver ${name}`);
379
540
  }
380
- catch (error) {
381
- fail("turn boundary", error);
541
+ if (spec.readyState === "ready" && this.#loggedDriver.get(name) !== "ready") {
542
+ this.#loggedDriver.set(name, "ready");
543
+ step(`driver ${name} ready`);
544
+ }
545
+ if (spec.readyState === "failed") {
546
+ const detail = spec.error === "" ? "failed" : spec.error;
547
+ if (this.#loggedError.get(`driver:${name}`) !== detail) {
548
+ this.#loggedError.set(`driver:${name}`, detail);
549
+ fail(`driver ${name}`, detail);
550
+ }
382
551
  }
383
- const ending = turn.cancelRequested
384
- ? { kind: "cancel" }
385
- : broke === ""
386
- ? { kind: "finish" }
387
- : { kind: "broke", detail: broke };
388
- closeTurn(turn, ending, this.#closeOptions());
389
- this.#releaseUnansweredCall(turn);
390
- // Self-service lane: the turn that was head takes the next queued message itself.
391
- // The prompt-queue reaction stays as the fallback (a restart re-arms it).
392
- void this.sessionDaemon.drainPromptQueue();
393
552
  }
394
- }
395
- }, { seal: false });
396
- this.#disposeLive = autorun(() => {
397
- if (!this.activated || this.#closed)
398
- return;
399
- const reachable = this.reachableJobs;
400
- for (const job of this.inFlightJobs()) {
401
- if (!(job instanceof SessionTurn) || job.reducerExited || reachable.includes(job))
402
- continue;
403
- failLeftoverApprovables(job, (leaf) => this.#abortLeaf(leaf), this.sessionDaemon.kernel.catalog);
404
- }
405
- });
406
- // Honor refuses an unstamped completion (`shouldHonor`). An executor would
407
- // stamp `orchestrationResult` or `completed` on every historical round.
408
- // Snapshot at activate is the recovery set; only a round that appears after
409
- // that is a new attempt, including an isolated `.prompt`.
410
- const stopBaseline = reaction(() => this.activated, (activated) => {
411
- if (!activated || this.#completionBaseline !== null)
553
+ for (const connectionId of Object.keys(this.connections)) {
554
+ const spec = this.connections[connectionId];
555
+ if (spec === undefined)
556
+ continue;
557
+ // Catalog is report. Honor ensures a wire when work names it.
558
+ const live = spec.live;
559
+ const models = Array.isArray(live.models) ? live.models.map(String) : [];
560
+ const can = spec.driver?.abilityClaim ?? [];
561
+ const liveSig = `${spec.readyState}|${can.join(",")}|${models.length}|${models.slice(0, 8).join(",")}`;
562
+ if (this.#loggedError.get(`live:${connectionId}`) !== liveSig) {
563
+ this.#loggedError.set(`live:${connectionId}`, liveSig);
564
+ const authoring = spec.__doc__;
565
+ const transport = this.kernel.doc;
566
+ step(`kernel live: connection=${connectionId} readyState=${spec.readyState} can=${can.join(",") || "(none)"} models=${models.length} ${models.length === 0 ? "(none)" : models.slice(0, 8).join(",")} shadow=${docTag(authoring)} main=${docTag(transport)} sameDoc=${authoring === transport}`);
567
+ }
568
+ if (spec.readyState !== "failed")
569
+ continue;
570
+ const detail = spec.error === "" ? "failed" : spec.error;
571
+ if (this.#loggedError.get(connectionId) === detail)
572
+ continue;
573
+ this.#loggedError.set(connectionId, detail);
574
+ fail(`connection ${connectionId}`, detail);
575
+ }
576
+ });
577
+ this.#disposeBoundWire = reaction(() => (this.activated ? (this.session.root.kvs.connection ?? "").trim() : ""), (id) => {
578
+ if (!this.activated || this.#closed || id === "" || this.connections[id] === undefined)
579
+ return;
580
+ void this.ensure(id);
581
+ }, { fireImmediately: true });
582
+ }
583
+ async #runMcp(job, signal) {
584
+ signal = this.#ownedSignal(signal);
585
+ if (!this.canApplyResults)
412
586
  return;
413
- this.#completionBaseline = new Set(this.#openUnstartedCompletions());
414
- });
415
- const stopDrive = reaction(() => this.#openUnstartedCompletions(), (jobs) => {
416
- const baseline = this.#completionBaseline;
417
- if (baseline === null)
587
+ if (this.#outsideWorkerCeiling(job)) {
588
+ closeMcpFailure(job, OUTSIDE_WORKER_CEILING);
418
589
  return;
419
- for (const job of jobs) {
420
- if (baseline.has(job))
421
- continue;
422
- baseline.add(job);
423
- const pew = job.expectation;
424
- void this.complete(completionRequest(pew)).catch((error) => {
425
- if (pew.isDone || this.#flights.aborted(pew))
426
- return;
427
- pew.fail("crash", error instanceof Error ? error.message : String(error), "null");
428
- });
429
590
  }
430
- }, { equals: comparer.shallow });
431
- this.#disposeCompletions = () => {
432
- stopBaseline();
433
- stopDrive();
434
- };
435
- this.#disposeConnections = autorun(() => {
436
- if (!this.activated || this.#closed)
591
+ if (mcpSessionPersistDenies(job.connectionKey, job.serverToolName, this.session.root.toolBlocklist)) {
592
+ closeMcpFailure(job, `unknown MCP tool "${job.serverToolName.trim() || job.name}"`);
437
593
  return;
438
- for (const name of Object.keys(this.drivers)) {
439
- const spec = this.drivers[name];
440
- if (spec === undefined)
441
- continue;
442
- if (!this.#loggedDriver.has(name)) {
443
- this.#loggedDriver.set(name, "");
444
- step(`driver ${name}`);
594
+ }
595
+ if (isPrivatePluginConnection(job.connectionKey)) {
596
+ const parsed = parsePrivateConnectionKey(job.connectionKey);
597
+ const recipe = this.session.root.agent.spec.name;
598
+ if (parsed == null || recipe === "") {
599
+ closeMcpFailure(job, "private MCP tool not admitted");
600
+ return;
445
601
  }
446
- if (spec.readyState === "ready" && this.#loggedDriver.get(name) !== "ready") {
447
- this.#loggedDriver.set(name, "ready");
448
- step(`driver ${name} ready`);
602
+ const admission = admitPrivateMcpTool(this.sessionDaemon.kernel.catalog.pluginInstall, recipe, parsed.pluginName, parsed.serverName, job.serverToolName);
603
+ if (!admission.allowed) {
604
+ closeMcpFailure(job, admission.reason ?? "private MCP tool not admitted");
605
+ return;
449
606
  }
450
- if (spec.readyState === "failed") {
451
- const detail = spec.error === "" ? "failed" : spec.error;
452
- if (this.#loggedError.get(`driver:${name}`) !== detail) {
453
- this.#loggedError.set(`driver:${name}`, detail);
454
- fail(`driver ${name}`, detail);
607
+ if (job.serverToolName === "commit") {
608
+ const requestId = job.argString("requestId");
609
+ const open = loadOpenCall(this.sessionDaemon.kernel, this.session.root, requestId);
610
+ if (open == null) {
611
+ closeMcpFailure(job, "commit requestId is not an open writer Call");
612
+ return;
455
613
  }
456
614
  }
457
615
  }
458
- for (const connectionId of Object.keys(this.connections)) {
459
- const spec = this.connections[connectionId];
460
- if (spec === undefined)
461
- continue;
462
- // Catalog is report. Honor ensures a wire when work names it.
463
- const live = spec.live;
464
- const models = Array.isArray(live.models) ? live.models.map(String) : [];
465
- const can = spec.driver?.abilityClaim ?? [];
466
- const liveSig = `${spec.readyState}|${can.join(",")}|${models.length}|${models.slice(0, 8).join(",")}`;
467
- if (this.#loggedError.get(`live:${connectionId}`) !== liveSig) {
468
- this.#loggedError.set(`live:${connectionId}`, liveSig);
469
- const authoring = spec.__doc__;
470
- const transport = this.kernel.doc;
471
- step(`kernel live: connection=${connectionId} readyState=${spec.readyState} can=${can.join(",") || "(none)"} models=${models.length} ${models.length === 0 ? "(none)" : models.slice(0, 8).join(",")} shadow=${docTag(authoring)} main=${docTag(transport)} sameDoc=${authoring === transport}`);
616
+ const resumed = await this.#inboundResumed(hostingTurnOf(this.session.root, job) ?? this.session.root.head, signal);
617
+ if (!this.canApplyResults)
618
+ return;
619
+ if (!resumed) {
620
+ if (isParkAbort(signal))
621
+ return;
622
+ closeMcpFailure(job, signal.aborted ? cancelOutcomeText(signal.reason) : "contribution paused");
623
+ return;
624
+ }
625
+ if (signal.aborted) {
626
+ if (isParkAbort(signal)) {
627
+ await waitDispatchedMcp(job);
628
+ if (this.canApplyResults)
629
+ sealMcpIfSettled(job);
630
+ return;
472
631
  }
473
- if (spec.readyState !== "failed")
474
- continue;
475
- const detail = spec.error === "" ? "failed" : spec.error;
476
- if (this.#loggedError.get(connectionId) === detail)
477
- continue;
478
- this.#loggedError.set(connectionId, detail);
479
- fail(`connection ${connectionId}`, detail);
632
+ closeMcpFailure(job, cancelOutcomeText(signal.reason));
633
+ return;
480
634
  }
481
- });
482
- this.#disposeBoundWire = reaction(() => (this.activated ? (this.session.root.kvs.connection ?? "").trim() : ""), (id) => {
483
- if (!this.activated || this.#closed || id === "" || this.connections[id] === undefined)
635
+ const key = job.connectionKey.trim();
636
+ if (key === "") {
637
+ const name = job.serverToolName.trim() || job.name.trim() || "mcp";
638
+ closeMcpFailure(job, `${name}: missing MCP connection`);
484
639
  return;
485
- void this.ensure(id);
486
- }, { fireImmediately: true });
487
- }
488
- async #runMcp(job, signal) {
489
- if (this.#outsideWorkerCeiling(job)) {
490
- closeMcpFailure(job, OUTSIDE_WORKER_CEILING);
491
- return;
492
- }
493
- if (mcpSessionPersistDenies(job.connectionKey, job.serverToolName, this.session.root.toolBlocklist)) {
494
- closeMcpFailure(job, `unknown MCP tool "${job.serverToolName.trim() || job.name}"`);
495
- return;
496
- }
497
- if (isPrivatePluginConnection(job.connectionKey)) {
498
- const parsed = parsePrivateConnectionKey(job.connectionKey);
499
- const recipe = this.session.root.agent.spec.name;
500
- if (parsed == null || recipe === "") {
501
- closeMcpFailure(job, "private MCP tool not admitted");
640
+ }
641
+ if (job.dispatched && !job.completed) {
642
+ const settled = await this.#settleLeftoverPew(job.expectation, key, "MCP connection", signal);
643
+ if (!this.canApplyResults)
644
+ return;
645
+ if (settled === "settled") {
646
+ sealMcpIfSettled(job);
647
+ return;
648
+ }
649
+ if (isParkAbort(signal)) {
650
+ await waitDispatchedMcp(job);
651
+ if (this.canApplyResults)
652
+ sealMcpIfSettled(job);
653
+ return;
654
+ }
655
+ job.outcomeSnap ??= { ok: false, text: cancelOutcomeText(signal.reason) };
656
+ this.#terminatePew(job.expectation);
657
+ job.markComplete();
502
658
  return;
503
659
  }
504
- const admission = admitPrivateMcpTool(this.sessionDaemon.kernel.catalog.pluginInstall, recipe, parsed.pluginName, parsed.serverName, job.serverToolName);
505
- if (!admission.allowed) {
506
- closeMcpFailure(job, admission.reason ?? "private MCP tool not admitted");
660
+ const missing = this.#connectionFailure(key, "MCP connection");
661
+ if (missing !== null) {
662
+ closeMcpFailure(job, missing);
507
663
  return;
508
664
  }
509
- if (job.serverToolName === "commit") {
510
- const requestId = job.argString("requestId");
511
- const open = loadOpenCall(this.sessionDaemon.kernel, this.session.root, requestId);
512
- if (open == null) {
513
- closeMcpFailure(job, "commit requestId is not an open writer Call");
665
+ try {
666
+ const instance = (await this.ensure(key));
667
+ if (!this.canApplyResults)
668
+ return;
669
+ if (!connectionSupports(instance, "mcp") || instance.callTool == null) {
670
+ closeMcpFailure(job, `${key}: missing MCP connection`);
671
+ return;
672
+ }
673
+ const args = job.args;
674
+ if (args === null) {
675
+ closeMcpFailure(job, "mcp: arguments");
676
+ return;
677
+ }
678
+ const result = await instance.callTool({
679
+ serverToolName: job.serverToolName,
680
+ args,
681
+ signal,
682
+ onDispatch: () => {
683
+ if (!this.canApplyResults)
684
+ throw new SessionOwnershipError();
685
+ job.dispatched = true;
686
+ },
687
+ });
688
+ if (!this.canApplyResults)
689
+ return;
690
+ job.outcomeSnap ??= result;
691
+ await this.#behavior.finishTool(job, signal);
692
+ if (this.canApplyResults && !job.completed && !job.cancelled)
693
+ job.markComplete();
694
+ }
695
+ catch (error) {
696
+ if (!this.canApplyResults)
697
+ return;
698
+ if (isParkAbort(signal)) {
699
+ await waitDispatchedMcp(job);
700
+ if (this.canApplyResults)
701
+ sealMcpIfSettled(job);
514
702
  return;
515
703
  }
704
+ if (error instanceof ConnectionMiss) {
705
+ closeMcpFailure(job, error.code === "noconnect" ? `${key}: missing MCP connection` : error.message);
706
+ return;
707
+ }
708
+ closeMcpFailure(job, signal.aborted ? cancelOutcomeText(signal.reason) : error instanceof Error ? error.message : String(error));
516
709
  }
517
710
  }
518
- if (!(await this.#inboundResumed(hostingTurnOf(this.session.root, job) ?? this.session.root.head, signal))) {
519
- if (isParkAbort(signal))
520
- return;
521
- closeMcpFailure(job, signal.aborted ? cancelOutcomeText(signal.reason) : "contribution paused");
522
- return;
711
+ /**
712
+ * A paused inbound call holds its turn's work. True when the call is not
713
+ * paused or resumed while waiting; false when it ended, stopped, or aborted.
714
+ */
715
+ async #inboundResumed(turn, signal, stopped = () => false) {
716
+ const inboundId = turn?.agentRequestId.trim() ?? "";
717
+ if (inboundId === "")
718
+ return true;
719
+ const catalog = this.sessionDaemon.kernel.catalog;
720
+ const inbound = loadOpenCall(this.sessionDaemon.kernel, this.session.root, inboundId);
721
+ if (inbound == null || !isCallPaused(inbound, catalog))
722
+ return true;
723
+ await waitUntil(() => !isCallPaused(inbound, catalog) || inbound.completed || stopped(), signal);
724
+ return !signal.aborted && !inbound.completed && !isCallPaused(inbound, catalog);
523
725
  }
524
- if (signal.aborted) {
525
- if (isParkAbort(signal)) {
526
- await waitDispatchedMcp(job);
527
- sealMcpIfSettled(job);
528
- return;
726
+ /**
727
+ * Covering receipt before BODY. Failed write is not dispatch. Recheck
728
+ * cancel after the receipt — local mutation is not a receipt.
729
+ */
730
+ async #gateIntent(job, signal) {
731
+ if (!this.canApplyResults)
732
+ return { dispatch: false, persistFailed: false };
733
+ const documents = this.sessionDaemon.kernel.runtime.documents;
734
+ if (documents != null) {
735
+ const gate = await persistJobIntent(documents, documentKey("sessions", this.session.root.sessionId), job);
736
+ if (!this.canApplyResults)
737
+ return { dispatch: false, persistFailed: false };
738
+ if (!gate.ok)
739
+ return { dispatch: false, persistFailed: true };
740
+ }
741
+ if (job.cancelled ||
742
+ job.completed ||
743
+ signal.aborted ||
744
+ hostingTurnOf(this.session.root, job)?.cancelRequested === true) {
745
+ return { dispatch: false, persistFailed: false };
529
746
  }
530
- closeMcpFailure(job, cancelOutcomeText(signal.reason));
531
- return;
747
+ return { dispatch: true };
532
748
  }
533
- const key = job.connectionKey.trim();
534
- if (key === "") {
535
- const name = job.serverToolName.trim() || job.name.trim() || "mcp";
536
- closeMcpFailure(job, `${name}: missing MCP connection`);
537
- return;
749
+ /**
750
+ * A BODY the gate refused still settles and runs its finish step, so the
751
+ * turn's join returns. A park leaves the job open for the next wrap.
752
+ */
753
+ async #bodyRefused(job, signal) {
754
+ const intent = await this.#gateIntent(job, signal);
755
+ if (!this.canApplyResults)
756
+ return true;
757
+ if (intent.dispatch)
758
+ return false;
759
+ if (!job.completed && (intent.persistFailed || !isParkAbort(signal))) {
760
+ const text = intent.persistFailed ? INTENT_NOT_PERSISTED : cancelOutcomeText(signal.reason);
761
+ job.outcomeSnap ??= { ok: false, text };
762
+ if (job instanceof MCPToolJob && !job.expectation.isDone)
763
+ job.expectation.fail("crash", text, "null");
764
+ job.markComplete();
765
+ }
766
+ await this.#behavior.finishTool(job, signal);
767
+ return true;
538
768
  }
539
- if (job.dispatched && !job.completed) {
540
- if ((await this.#settleLeftoverPew(job.expectation, key, "MCP connection", signal)) === "settled") {
541
- sealMcpIfSettled(job);
769
+ /** The two web tools differ only in their argument, their connection pick, and the call. */
770
+ async #runWeb(job, signal) {
771
+ signal = this.#ownedSignal(signal);
772
+ if (await this.#bodyRefused(job, signal))
542
773
  return;
543
- }
544
- if (isParkAbort(signal)) {
545
- await waitDispatchedMcp(job);
546
- sealMcpIfSettled(job);
774
+ if (!this.canApplyResults)
547
775
  return;
776
+ const web = job instanceof WebSearchToolJob
777
+ ? {
778
+ name: "web_search",
779
+ ability: "web-search",
780
+ missing: job.query === "" ? "query is required — call web_search again with the text to search for" : "",
781
+ pick: pickWebSearchConnection,
782
+ call: (instance) => instance.webSearch?.({ query: job.query, limit: job.limit, signal }),
783
+ }
784
+ : {
785
+ name: "web_extract",
786
+ ability: "web-extract",
787
+ missing: job.url === "" ? "url is required — call web_extract again with the page url" : "",
788
+ pick: pickWebExtractConnection,
789
+ call: (instance) => instance.webExtract?.({ url: job.url, charLimit: job.charLimit, signal }),
790
+ };
791
+ try {
792
+ if (this.#outsideWorkerCeiling(job)) {
793
+ job.outcomeSnap ??= { ok: false, text: OUTSIDE_WORKER_CEILING };
794
+ return;
795
+ }
796
+ if (signal.aborted) {
797
+ job.outcomeSnap ??= { ok: false, text: cancelOutcomeText(signal.reason) };
798
+ return;
799
+ }
800
+ if (web.missing !== "") {
801
+ job.outcomeSnap ??= { ok: false, text: `${web.name}: ${web.missing}` };
802
+ return;
803
+ }
804
+ const key = web.pick(this.kernel.root);
805
+ if (key === null) {
806
+ job.outcomeSnap ??= {
807
+ ok: false,
808
+ text: `${web.name}: no ${web.ability} connection — /connect a provider or leave the keyless ring on`,
809
+ };
810
+ return;
811
+ }
812
+ job.connectionKey = key;
813
+ if (job.expectation.startedAtMs == null)
814
+ job.expectation.connectionId = key;
815
+ const leftoverPew = job.expectation;
816
+ if (leftoverPew.startedAtMs != null && !leftoverPew.isDone) {
817
+ const settled = await this.#settleLeftoverPew(leftoverPew, key, `${web.ability} connection`, signal);
818
+ if (!this.canApplyResults)
819
+ return;
820
+ if (settled === "aborted") {
821
+ job.outcomeSnap ??= { ok: false, text: cancelOutcomeText(signal.reason) };
822
+ this.#terminatePew(leftoverPew);
823
+ }
824
+ return;
825
+ }
826
+ try {
827
+ const instance = (await this.ensure(key));
828
+ if (!this.canApplyResults)
829
+ return;
830
+ const found = connectionSupports(instance, web.ability) ? await web.call(instance) : undefined;
831
+ if (!this.canApplyResults)
832
+ return;
833
+ job.outcomeSnap ??= found ?? { ok: false, text: `${web.name}: intern failed` };
834
+ }
835
+ catch (error) {
836
+ if (!this.canApplyResults)
837
+ return;
838
+ job.outcomeSnap ??= {
839
+ ok: false,
840
+ text: signal.aborted
841
+ ? cancelOutcomeText(signal.reason)
842
+ : error instanceof Error
843
+ ? error.message
844
+ : String(error),
845
+ };
846
+ }
847
+ }
848
+ finally {
849
+ if (this.canApplyResults) {
850
+ await this.#behavior.finishTool(job, signal);
851
+ if (this.canApplyResults)
852
+ this.#sealWebJob(job);
853
+ }
548
854
  }
549
- job.outcomeSnap ??= { ok: false, text: cancelOutcomeText(signal.reason) };
550
- this.#terminatePew(job.expectation);
551
- job.markComplete();
552
- return;
553
855
  }
554
- const missing = this.#connectionFailure(key, "MCP connection");
555
- if (missing !== null) {
556
- closeMcpFailure(job, missing);
557
- return;
856
+ /** A named worker runs only the tools its catalog lists. */
857
+ #outsideWorkerCeiling(job) {
858
+ const root = this.session.root;
859
+ return root.agent.spec.name !== "" && sessionCatalog(root)?.catalogEntry(job.name) == null;
558
860
  }
559
- try {
560
- const instance = (await this.ensure(key));
561
- if (!connectionSupports(instance, "mcp") || instance.callTool == null) {
562
- closeMcpFailure(job, `${key}: missing MCP connection`);
563
- return;
564
- }
565
- const args = job.args;
566
- if (args === null) {
567
- closeMcpFailure(job, "mcp: arguments");
861
+ /** Why `key` cannot serve, or null while it may. */
862
+ #connectionFailure(key, what) {
863
+ const spec = this.connections[key];
864
+ if (spec === undefined)
865
+ return `${key}: missing ${what}`;
866
+ if (spec.readyState === "failed")
867
+ return spec.error === "" ? `${key}: failed` : spec.error;
868
+ return null;
869
+ }
870
+ /**
871
+ * A PEW an earlier wrap started settles on its own; it fails here only when
872
+ * its connection is gone. Aborted leaves the PEW to the caller.
873
+ */
874
+ async #settleLeftoverPew(pew, key, what, signal) {
875
+ const settled = await waitUntil(() => pew.isDone || this.#connectionFailure(key, what) !== null, signal);
876
+ if (!this.canApplyResults)
877
+ return "aborted";
878
+ if (settled === "aborted")
879
+ return settled;
880
+ const detail = this.#connectionFailure(key, what);
881
+ if (detail !== null && !pew.isDone)
882
+ pew.fail("crash", detail, "null");
883
+ return settled;
884
+ }
885
+ #sealWebJob(job) {
886
+ if (job.completed || job.cancelled)
568
887
  return;
888
+ if ((job.receiptJson != null && job.receiptJson !== "") || job.expectation.isDone) {
889
+ job.markComplete();
569
890
  }
570
- const result = await instance.callTool({
571
- serverToolName: job.serverToolName,
572
- args,
573
- signal,
574
- onDispatch: () => {
575
- job.dispatched = true;
891
+ }
892
+ /**
893
+ * Guard phase of a host tool. The session's plugin guards wrap the license:
894
+ * a guard may refuse, answer without running the tool, or narrow the
895
+ * arguments, which are written back before the license judges them.
896
+ * Pre hooks run once the call is licensed.
897
+ */
898
+ async #guardTool(job) {
899
+ if (!this.canApplyResults)
900
+ return false;
901
+ // Origin-aware door. Scheme inner admits and RPC/statusline still enter
902
+ // here as model until those callers pass their origin through this path.
903
+ const result = await guardHostTool({
904
+ job,
905
+ signal: this.#behavior.signalFor(job),
906
+ stopped: () => !this.canApplyResults || !isWorkReachable(this.session.root, job),
907
+ origin: "model",
908
+ hookTimeoutMs: pluginHookTimeoutMs(this.session.root.kvs, this.kernel.root.kvs),
909
+ agent: this.#behavior.agent,
910
+ behaviorEntries: behaviorEntriesThatRun(behaviorEntriesOf(this.session.root), (wire) => sessionCatalog(this.session.root)?.catalogEntry(wire)),
911
+ guardedCall: (leaf) => this.#guardedCall(leaf),
912
+ license: (leaf) => this.#license(leaf),
913
+ turnId: (hostingTurnOf(this.session.root, job) ?? this.session.root.head)?.uuid ?? "",
914
+ narrowAgent: (leaf, tools) => this.#narrowAgent(leaf, tools),
915
+ onUnentered: (leaf, outcome) => {
916
+ if (this.canApplyResults)
917
+ stampUnentered(leaf, outcome);
576
918
  },
919
+ onProblem: (what, error) => this.#behavior.problem(what, error),
920
+ onReport: (leaf, outcome) => this.#behavior.toolReport(leaf, outcome),
577
921
  });
578
- job.outcomeSnap ??= result;
579
- await this.#behavior.finishTool(job, signal);
580
- if (!job.completed && !job.cancelled)
581
- job.markComplete();
922
+ if (!this.canApplyResults) {
923
+ result.opened?.abandon();
924
+ return false;
925
+ }
926
+ if (result.opened != null)
927
+ this.#behavior.holdTool(job, result.opened);
928
+ return result.allowed;
582
929
  }
583
- catch (error) {
584
- if (isParkAbort(signal)) {
585
- await waitDispatchedMcp(job);
586
- sealMcpIfSettled(job);
930
+ #guardedCall(job) {
931
+ if (job instanceof BotCallJob) {
932
+ // Named bot calls retain the existing agent guard point, without child tool grants.
933
+ if (job.name !== AGENT_ASK && job.name !== AGENT_SEND)
934
+ return guardedCallOf(job, this.session.root.path);
935
+ const spec = job.botName || job.contributionBotName || job.argString("spec").trim();
936
+ const catalog = sessionCatalog(this.session.root) ?? this.sessionDaemon.kernel.catalog;
937
+ const recipe = catalog.recipe(spec);
938
+ return recipe == null ? null : agentGuardedCall(job, spec, recipe.spec.tools, false);
939
+ }
940
+ if (!(job instanceof AgentCallJob))
941
+ return guardedCallOf(job, this.session.root.path);
942
+ if (job.name !== AGENT_ASK && job.name !== AGENT_SEND)
943
+ return null;
944
+ const spec = job.argString("spec").trim();
945
+ const fresh = job.workerSessionId === "";
946
+ const catalog = sessionCatalog(this.session.root) ?? this.sessionDaemon.kernel.catalog;
947
+ if (spec === "") {
948
+ const tools = job.inheritCallerTools ? catalog.catalogWireNames() : job.tools;
949
+ return agentGuardedCall(job, "", tools, fresh);
950
+ }
951
+ const recipe = catalog.recipe(spec);
952
+ return recipe == null ? null : agentGuardedCall(job, spec, recipe.spec.tools, fresh);
953
+ }
954
+ /** An ad-hoc ask carries its tools on the job; a one-shot recipe worker takes them at mint. */
955
+ #narrowAgent(job, tools) {
956
+ if (!this.canApplyResults)
957
+ return;
958
+ if (job.argString("spec").trim() === "") {
959
+ job.tools = [...tools];
960
+ job.inheritCallerTools = false;
587
961
  return;
588
962
  }
589
- if (error instanceof ConnectionMiss) {
590
- closeMcpFailure(job, error.code === "noconnect" ? `${key}: missing MCP connection` : error.message);
963
+ this.#agentTools.set(job, tools);
964
+ // A call cancelled before its worker is minted never takes these.
965
+ void when(() => job.completed || job.cancelled).then(() => this.#agentTools.delete(job));
966
+ }
967
+ async #finishCall(job, signal) {
968
+ if (!this.canApplyResults)
591
969
  return;
970
+ if (job instanceof AgentCallJob)
971
+ this.#agentTools.delete(job);
972
+ await this.#behavior.finishCall(job, signal);
973
+ }
974
+ async #license(job) {
975
+ if (!this.canApplyResults)
976
+ return false;
977
+ const started = Date.now();
978
+ trace("harness:license", `wait ${job.toolCallId || job.name} tool=${job.name} policy=${job.policyResult ?? "pending"}`);
979
+ if (licenseDecisionNow(job) === false) {
980
+ stampLicenseDeny(job, this.session.root.permissionMode);
981
+ await noteDenied(job, this.session.root);
982
+ trace("harness:license", `deny ${job.toolCallId || job.name}`);
983
+ return false;
592
984
  }
593
- closeMcpFailure(job, signal.aborted ? cancelOutcomeText(signal.reason) : error instanceof Error ? error.message : String(error));
985
+ const hostTurn = hostingTurnOf(this.session.root, job) ?? this.session.root.head;
986
+ const ok = await license(job, hostTurn, this.session.root);
987
+ if (!this.canApplyResults)
988
+ return false;
989
+ trace("harness:license", `${ok ? "allow" : "deny"} ${job.toolCallId || job.name} ms=${Date.now() - started} policy=${job.policyResult ?? ""} judge=${job.classifierResult ?? ""}`);
990
+ if (ok || job.cancelled || job.completed)
991
+ return ok;
992
+ const decision = licenseDecisionNow(job);
993
+ if (decision === false)
994
+ stampLicenseDeny(job, this.session.root.permissionMode);
995
+ else if (!this.#closed)
996
+ cancelUnstartedTool(job);
997
+ return false;
594
998
  }
595
- }
596
- /**
597
- * A paused inbound call holds its turn's work. True when the call is not
598
- * paused or resumed while waiting; false when it ended, stopped, or aborted.
599
- */
600
- async #inboundResumed(turn, signal, stopped = () => false) {
601
- const inboundId = turn?.agentRequestId.trim() ?? "";
602
- if (inboundId === "")
603
- return true;
604
- const catalog = this.sessionDaemon.kernel.catalog;
605
- const inbound = loadOpenCall(this.sessionDaemon.kernel, this.session.root, inboundId);
606
- if (inbound == null || !isCallPaused(inbound, catalog))
607
- return true;
608
- await waitUntil(() => !isCallPaused(inbound, catalog) || inbound.completed || stopped(), signal);
609
- return !signal.aborted && !inbound.completed && !isCallPaused(inbound, catalog);
610
- }
611
- /**
612
- * Covering receipt before BODY. Failed write is not dispatch. Recheck
613
- * cancel after the receipt — local mutation is not a receipt.
614
- */
615
- async #gateIntent(job, signal) {
616
- const documents = this.sessionDaemon.kernel.runtime.documents;
617
- if (documents != null) {
618
- const gate = await persistJobIntent(documents, documentKey("sessions", this.session.root.sessionId), job);
619
- if (!gate.ok)
620
- return { dispatch: false, persistFailed: true };
999
+ #agentHost() {
1000
+ return {
1001
+ kernel: this.sessionDaemon.kernel,
1002
+ project: this.sessionDaemon.project,
1003
+ caller: this.session.root,
1004
+ problem: (what, error) => this.#behavior.problem(what, error),
1005
+ canApplyResults: () => this.canApplyResults,
1006
+ ownershipSignal: this.sessionDaemon.sessionRuntime.ownershipSignal,
1007
+ };
621
1008
  }
622
- if (job.cancelled ||
623
- job.completed ||
624
- signal.aborted ||
625
- hostingTurnOf(this.session.root, job)?.cancelRequested === true) {
626
- return { dispatch: false, persistFailed: false };
1009
+ #permissionDesk() {
1010
+ if (!delegatesPermissionMode(this.session.root))
1011
+ return null;
1012
+ const id = permissionDeskSessionId(this.session.root, this.kernel.root);
1013
+ if (id === "" || id === this.session.root.sessionId)
1014
+ return null;
1015
+ return this.sessionDaemon.kernel.sessionPlexus(id)?.root ?? null;
627
1016
  }
628
- return { dispatch: true };
629
- }
630
- /**
631
- * A BODY the gate refused still settles and runs its finish step, so the
632
- * turn's join returns. A park leaves the job open for the next wrap.
633
- */
634
- async #bodyRefused(job, signal) {
635
- const intent = await this.#gateIntent(job, signal);
636
- if (intent.dispatch)
637
- return false;
638
- if (!job.completed && (intent.persistFailed || !isParkAbort(signal))) {
639
- const text = intent.persistFailed ? INTENT_NOT_PERSISTED : cancelOutcomeText(signal.reason);
640
- job.outcomeSnap ??= { ok: false, text };
641
- if (job instanceof MCPToolJob && !job.expectation.isDone)
642
- job.expectation.fail("crash", text, "null");
643
- job.markComplete();
1017
+ #deskPermissionSnap() {
1018
+ const desk = this.#permissionDesk();
1019
+ return desk == null ? null : deskPermissionSnap(desk);
644
1020
  }
645
- await this.#behavior.finishTool(job, signal);
646
- return true;
647
- }
648
- /** The two web tools differ only in their argument, their connection pick, and the call. */
649
- async #runWeb(job, signal) {
650
- if (await this.#bodyRefused(job, signal))
651
- return;
652
- const web = job instanceof WebSearchToolJob
653
- ? {
654
- name: "web_search",
655
- ability: "web-search",
656
- missing: job.query === "" ? "query is required — call web_search again with the text to search for" : "",
657
- pick: pickWebSearchConnection,
658
- call: (instance) => instance.webSearch?.({ query: job.query, limit: job.limit, signal }),
659
- }
660
- : {
661
- name: "web_extract",
662
- ability: "web-extract",
663
- missing: job.url === "" ? "url is required — call web_extract again with the page url" : "",
664
- pick: pickWebExtractConnection,
665
- call: (instance) => instance.webExtract?.({ url: job.url, charLimit: job.charLimit, signal }),
1021
+ ensure(connectionId) {
1022
+ if (connectionId === NOUS_GATEWAY_CONNECTION_ID)
1023
+ return this.#ensureBorrowedGateway();
1024
+ return this.#loggedEnsure(connectionId, this.isEnsured(connectionId), super.ensure(connectionId));
1025
+ }
1026
+ /**
1027
+ * `web-nous` is one intern id whose person can move. Memo by intern+source;
1028
+ * {@link dropConnection} is the remint door so B is not A's lease.
1029
+ */
1030
+ #ensureBorrowedGateway() {
1031
+ const connectionId = NOUS_GATEWAY_CONNECTION_ID;
1032
+ const source = nousGatewaySourceOf(this.kernel.root, connectionId);
1033
+ const tracked = this.#gatewayEnsure.has(borrowedEnsureKey(connectionId, source));
1034
+ return this.#loggedEnsure(connectionId, tracked, memoBorrowedEnsure(this.#gatewayEnsure, connectionId, source, (id) => this.ensure(id), () => super.ensure(connectionId), () => dropConnection(this.kernel.root, connectionId)));
1035
+ }
1036
+ #loggedEnsure(connectionId, tracked, pending) {
1037
+ if (tracked)
1038
+ return pending;
1039
+ step(`ensure ${connectionId}`);
1040
+ void pending.then(() => step(`ensure ${connectionId} ready`), (error) => fail(`ensure ${connectionId}`, error));
1041
+ return pending;
1042
+ }
1043
+ attachConnection(connectionId, instance) {
1044
+ const kernel = this.sessionDaemon.kernel;
1045
+ instance.bindSettingsWriter((patch) => {
1046
+ kernel.adjustConfig((config) => {
1047
+ if (!Object.hasOwn(config.connections, connectionId))
1048
+ return;
1049
+ const row = config.connections[connectionId];
1050
+ row.settings = { ...row.settings, ...patch };
1051
+ });
1052
+ });
1053
+ }
1054
+ connectionSecrets(connectionId) {
1055
+ const root = this.sessionDaemon.kernel.root;
1056
+ const ownerId = () => secretsConnectionId(this.kernel.root, connectionId);
1057
+ return {
1058
+ get: (field) => readConnectionSecrets(root, ownerId())[field],
1059
+ all: () => readConnectionSecrets(root, ownerId()),
1060
+ replace: (secrets) => {
1061
+ writeConnectionSecrets(root, ownerId(), { ...secrets });
1062
+ },
666
1063
  };
667
- try {
668
- if (this.#outsideWorkerCeiling(job)) {
669
- job.outcomeSnap ??= { ok: false, text: OUTSIDE_WORKER_CEILING };
670
- return;
671
- }
672
- if (signal.aborted) {
673
- job.outcomeSnap ??= { ok: false, text: cancelOutcomeText(signal.reason) };
674
- return;
675
- }
676
- if (web.missing !== "") {
677
- job.outcomeSnap ??= { ok: false, text: `${web.name}: ${web.missing}` };
678
- return;
679
- }
680
- const key = web.pick(this.kernel.root);
681
- if (key === null) {
682
- job.outcomeSnap ??= {
683
- ok: false,
684
- text: `${web.name}: no ${web.ability} connection — /connect a provider or leave the keyless ring on`,
685
- };
686
- return;
1064
+ }
1065
+ /**
1066
+ * Remainder / compact `.prompt` / turn port. Interns the row, then
1067
+ * `connection.complete` → `handle(round)`. `KernelDaemon` has no `complete()`.
1068
+ */
1069
+ complete(req) {
1070
+ if (!this.canApplyResults)
1071
+ return Promise.reject(new SessionOwnershipError());
1072
+ const ownership = this.sessionDaemon.sessionRuntime.ownershipSignal;
1073
+ const abort = new AbortController();
1074
+ // Losing the writer leaves recovery in charge of the open attempt.
1075
+ const onOwnershipLost = () => abort.abort("park");
1076
+ const onCallerAbort = () => abort.abort(ownership.aborted ? "park" : req.signal?.reason);
1077
+ ownership.addEventListener("abort", onOwnershipLost, { once: true });
1078
+ req.signal?.addEventListener("abort", onCallerAbort, { once: true });
1079
+ if (ownership.aborted)
1080
+ onOwnershipLost();
1081
+ else if (req.signal?.aborted)
1082
+ onCallerAbort();
1083
+ return this.#flights
1084
+ .complete({
1085
+ ...req,
1086
+ signal: abort.signal,
1087
+ })
1088
+ .finally(() => {
1089
+ ownership.removeEventListener("abort", onOwnershipLost);
1090
+ req.signal?.removeEventListener("abort", onCallerAbort);
1091
+ });
1092
+ }
1093
+ /**
1094
+ * Open completions honor will not start. `startedAtMs` means the attempt
1095
+ * already began. Tape completions belong to the turn resolution strategy. Only isolated
1096
+ * completions use this driver, so there is one dispatch owner per node.
1097
+ */
1098
+ #openUnstartedCompletions() {
1099
+ if (!this.activated || this.#closed || !this.canApplyResults || this.sessionDaemon.sessionRuntime.admissionHeld)
1100
+ return [];
1101
+ const open = [];
1102
+ for (const node of this.reachableJobs) {
1103
+ if (!(node instanceof CompletionJob))
1104
+ continue;
1105
+ const pew = node.expectation;
1106
+ if (pew.isDone || pew.startedAtMs != null)
1107
+ continue;
1108
+ const parent = node.parent;
1109
+ if (parent instanceof SessionTurn && (parent.queue.includes(node) || parent.doneWork.includes(node)))
1110
+ continue;
1111
+ open.push(node);
687
1112
  }
688
- job.connectionKey = key;
689
- if (job.expectation.startedAtMs == null)
690
- job.expectation.connectionId = key;
691
- const leftoverPew = job.expectation;
692
- if (leftoverPew.startedAtMs != null && !leftoverPew.isDone) {
693
- if ((await this.#settleLeftoverPew(leftoverPew, key, `${web.ability} connection`, signal)) === "aborted") {
694
- job.outcomeSnap ??= { ok: false, text: cancelOutcomeText(signal.reason) };
695
- this.#terminatePew(leftoverPew);
1113
+ return open;
1114
+ }
1115
+ #stopCompletions(reason) {
1116
+ this.#disposeCompletions();
1117
+ this.#flights.abortAll(reason);
1118
+ }
1119
+ async #driveCompletion(req) {
1120
+ if (!this.canApplyResults)
1121
+ throw new SessionOwnershipError();
1122
+ const owner = req.round.parent;
1123
+ if (owner instanceof CompletionJob) {
1124
+ if (owner.orchestrationResult == null) {
1125
+ runInAction(() => {
1126
+ owner.orchestrationResult = true;
1127
+ });
1128
+ }
1129
+ const intent = await this.#gateIntent(owner, req.signal ?? new AbortController().signal);
1130
+ if (!this.canApplyResults)
1131
+ throw new SessionOwnershipError();
1132
+ if (!intent.dispatch) {
1133
+ if (intent.persistFailed && req.round.outcome == null) {
1134
+ req.round.fail("crash", INTENT_NOT_PERSISTED, "null");
1135
+ }
1136
+ throw new Error(intent.persistFailed ? INTENT_NOT_PERSISTED : "completion cancelled");
696
1137
  }
697
- return;
698
1138
  }
1139
+ // The retry budget is read once, here, and recorded on the round. A live setting change
1140
+ // does not reach a round that is already retrying.
1141
+ req.round.stampRetryLimits({
1142
+ maxMs: this.kernel.root.retryMaxMs,
1143
+ maxAttempts: this.kernel.root.retryMaxAttempts,
1144
+ });
1145
+ const started = Date.now();
1146
+ const id = req.round.uuid;
1147
+ trace("harness:complete", `start ${id} connection=${req.connectionId} model=${req.model} isolated=${req.round.completionMode === "isolated"}`);
1148
+ this.originCompletions.add(req.round);
699
1149
  try {
700
- const instance = (await this.ensure(key));
701
- const found = connectionSupports(instance, web.ability) ? await web.call(instance) : undefined;
702
- job.outcomeSnap ??= found ?? { ok: false, text: `${web.name}: intern failed` };
1150
+ const result = await internedComplete(this, req);
1151
+ trace("harness:complete", `done ${id} ms=${Date.now() - started} chars=${result.text.length}`);
1152
+ return result;
703
1153
  }
704
1154
  catch (error) {
705
- job.outcomeSnap ??= {
706
- ok: false,
707
- text: signal.aborted
708
- ? cancelOutcomeText(signal.reason)
709
- : error instanceof Error
710
- ? error.message
711
- : String(error),
712
- };
1155
+ trace("harness:complete", `fail ${id} ms=${Date.now() - started} ${error instanceof Error ? error.message : String(error)}`);
1156
+ if (error instanceof ConnectionMiss)
1157
+ this.originCompletions.delete(req.round);
1158
+ throw error;
713
1159
  }
714
1160
  }
715
- finally {
716
- await this.#behavior.finishTool(job, signal);
717
- this.#sealWebJob(job);
1161
+ /**
1162
+ * A guard or body threw. The job's joiners (the turn's `joinReleased`, a
1163
+ * program's leaf wait) read the receipt, so the model sees the error as
1164
+ * the tool result and the turn continues instead of waiting forever.
1165
+ */
1166
+ stampCrash(job, phase, detail) {
1167
+ const text = `${phase === "license" ? "license crashed" : "tool crashed"}: ${detail === "" ? "unknown error" : detail}`;
1168
+ trace("harness:crash", `${job.__type__} ${phase} ${detail}`);
1169
+ if (job instanceof ToolCallJob)
1170
+ stampUnentered(job, { ok: false, text });
1171
+ else if (job instanceof ProgrammaticJob)
1172
+ job.outcomeSnap ??= { ok: false, text };
718
1173
  }
719
- }
720
- /** A named worker runs only the tools its catalog lists. */
721
- #outsideWorkerCeiling(job) {
722
- const root = this.session.root;
723
- return root.agent.spec.name !== "" && sessionCatalog(root)?.catalogEntry(job.name) == null;
724
- }
725
- /** Why `key` cannot serve, or null while it may. */
726
- #connectionFailure(key, what) {
727
- const spec = this.connections[key];
728
- if (spec === undefined)
729
- return `${key}: missing ${what}`;
730
- if (spec.readyState === "failed")
731
- return spec.error === "" ? `${key}: failed` : spec.error;
732
- return null;
733
- }
734
- /**
735
- * A PEW an earlier wrap started settles on its own; it fails here only when
736
- * its connection is gone. Aborted leaves the PEW to the caller.
737
- */
738
- async #settleLeftoverPew(pew, key, what, signal) {
739
- const settled = await waitUntil(() => pew.isDone || this.#connectionFailure(key, what) !== null, signal);
740
- if (settled === "aborted")
741
- return settled;
742
- const detail = this.#connectionFailure(key, what);
743
- if (detail !== null && !pew.isDone)
744
- pew.fail("crash", detail, "null");
745
- return settled;
746
- }
747
- #sealWebJob(job) {
748
- if (job.completed || job.cancelled)
749
- return;
750
- if ((job.receiptJson != null && job.receiptJson !== "") || job.expectation.isDone) {
751
- job.markComplete();
1174
+ shouldHonor(node) {
1175
+ if (!this.canApplyResults)
1176
+ return false;
1177
+ if (this.sessionDaemon.sessionRuntime.admissionHeld)
1178
+ return false;
1179
+ if (!super.shouldHonor(node))
1180
+ return false;
1181
+ if (node instanceof McpToolExpectation)
1182
+ return node.startedAtMs != null || node.dispatched;
1183
+ if (node instanceof CompletionExpectation)
1184
+ return node.startedAtMs != null;
1185
+ return true;
752
1186
  }
753
- }
754
- /**
755
- * Guard phase of a host tool. The session's plugin guards wrap the license:
756
- * a guard may refuse, answer without running the tool, or narrow the
757
- * arguments, which are written back before the license judges them.
758
- * Pre hooks run once the call is licensed.
759
- */
760
- async #guardTool(job) {
761
- // Origin-aware door. Scheme inner admits and RPC/statusline still enter
762
- // here as model until those callers pass their origin through this path.
763
- const result = await guardHostTool({
764
- job,
765
- origin: "model",
766
- agent: this.#behavior.agent,
767
- behaviorEntries: behaviorEntriesThatRun(behaviorEntriesOf(this.session.root), (wire) => sessionCatalog(this.session.root)?.catalogEntry(wire)),
768
- guardedCall: (leaf) => this.#guardedCall(leaf),
769
- license: (leaf) => this.#license(leaf),
770
- turnId: (hostingTurnOf(this.session.root, job) ?? this.session.root.head)?.uuid ?? "",
771
- narrowAgent: (leaf, tools) => this.#narrowAgent(leaf, tools),
772
- onUnentered: stampUnentered,
773
- onProblem: (what, error) => this.#behavior.problem(what, error),
774
- onReport: (leaf, outcome) => this.#behavior.toolReport(leaf, outcome),
775
- });
776
- if (result.opened != null)
777
- this.#behavior.holdTool(job, result.opened);
778
- return result.allowed;
779
- }
780
- #guardedCall(job) {
781
- if (job instanceof BotCallJob) {
782
- // Named bot calls retain the existing agent guard point, without child tool grants.
783
- if (job.name !== AGENT_ASK && job.name !== AGENT_SEND)
784
- return guardedCallOf(job, this.session.root.path);
785
- const spec = job.botName || job.contributionBotName || job.argString("spec").trim();
786
- const catalog = sessionCatalog(this.session.root) ?? this.sessionDaemon.kernel.catalog;
787
- const recipe = catalog.recipe(spec);
788
- return recipe == null ? null : agentGuardedCall(job, spec, recipe.spec.tools, false);
1187
+ get canApplyResults() {
1188
+ return super.canApplyResults && this.sessionDaemon.sessionRuntime.isCurrent;
789
1189
  }
790
- if (!(job instanceof AgentCallJob))
791
- return guardedCallOf(job, this.session.root.path);
792
- if (job.name !== AGENT_ASK && job.name !== AGENT_SEND)
793
- return null;
794
- const spec = job.argString("spec").trim();
795
- const fresh = job.workerSessionId === "";
796
- const catalog = sessionCatalog(this.session.root) ?? this.sessionDaemon.kernel.catalog;
797
- if (spec === "") {
798
- const tools = job.inheritCallerTools ? catalog.catalogWireNames() : job.tools;
799
- return agentGuardedCall(job, "", tools, fresh);
1190
+ #ownedSignal(signal) {
1191
+ return AbortSignal.any([signal, this.sessionDaemon.sessionRuntime.ownershipSignal]);
800
1192
  }
801
- const recipe = catalog.recipe(spec);
802
- return recipe == null ? null : agentGuardedCall(job, spec, recipe.spec.tools, fresh);
803
- }
804
- /** An ad-hoc ask carries its tools on the job; a one-shot recipe worker takes them at mint. */
805
- #narrowAgent(job, tools) {
806
- if (job.argString("spec").trim() === "") {
807
- job.tools = [...tools];
808
- job.inheritCallerTools = false;
809
- return;
1193
+ async loadDriverModule(driver) {
1194
+ return this.sessionDaemon.kernel.runtime.loadDriver(driver);
810
1195
  }
811
- this.#agentTools.set(job, tools);
812
- // A call cancelled before its worker is minted never takes these.
813
- void when(() => job.completed || job.cancelled).then(() => this.#agentTools.delete(job));
814
- }
815
- async #finishCall(job, signal) {
816
- if (job instanceof AgentCallJob)
817
- this.#agentTools.delete(job);
818
- await this.#behavior.finishCall(job, signal);
819
- }
820
- async #license(job) {
821
- const started = Date.now();
822
- trace("harness:license", `wait ${job.toolCallId || job.name} tool=${job.name} policy=${job.policyResult ?? "pending"}`);
823
- if (licenseDecisionNow(job) === false) {
824
- stampLicenseDeny(job, this.session.root.permissionMode);
825
- await noteDenied(job, this.session.root);
826
- trace("harness:license", `deny ${job.toolCallId || job.name}`);
827
- return false;
1196
+ #holdingDispatchedMcp = false;
1197
+ #holdingEval = false;
1198
+ #holdingCallbacks = false;
1199
+ get holdingEval() {
1200
+ return this.#holdingEval;
828
1201
  }
829
- const hostTurn = hostingTurnOf(this.session.root, job) ?? this.session.root.head;
830
- const ok = await license(job, hostTurn, this.session.root);
831
- trace("harness:license", `${ok ? "allow" : "deny"} ${job.toolCallId || job.name} ms=${Date.now() - started} policy=${job.policyResult ?? ""} judge=${job.classifierResult ?? ""}`);
832
- if (ok || job.cancelled || job.completed)
833
- return ok;
834
- const decision = licenseDecisionNow(job);
835
- if (decision === false)
836
- stampLicenseDeny(job, this.session.root.permissionMode);
837
- else if (!this.#closed)
838
- cancelUnstartedTool(job);
839
- return false;
840
- }
841
- #agentHost() {
842
- return {
843
- kernel: this.sessionDaemon.kernel,
844
- project: this.sessionDaemon.project,
845
- caller: this.session.root,
846
- problem: (what, error) => this.#behavior.problem(what, error),
847
- };
848
- }
849
- #permissionDesk() {
850
- if (!delegatesPermissionMode(this.session.root))
851
- return null;
852
- const id = permissionDeskSessionId(this.session.root, this.kernel.root);
853
- if (id === "" || id === this.session.root.sessionId)
854
- return null;
855
- return this.sessionDaemon.kernel.sessionPlexus(id)?.root ?? null;
856
- }
857
- #deskPermissionSnap() {
858
- const desk = this.#permissionDesk();
859
- return desk == null ? null : deskPermissionSnap(desk);
860
- }
861
- ensure(connectionId) {
862
- if (connectionId === NOUS_GATEWAY_CONNECTION_ID)
863
- return this.#ensureBorrowedGateway();
864
- return this.#loggedEnsure(connectionId, this.isEnsured(connectionId), super.ensure(connectionId));
865
- }
866
- /**
867
- * `web-nous` is one intern id whose person can move. Memo by intern+source;
868
- * {@link dropConnection} is the remint door so B is not A's lease.
869
- */
870
- #ensureBorrowedGateway() {
871
- const connectionId = NOUS_GATEWAY_CONNECTION_ID;
872
- const source = nousGatewaySourceOf(this.kernel.root, connectionId);
873
- const tracked = this.#gatewayEnsure.has(borrowedEnsureKey(connectionId, source));
874
- return this.#loggedEnsure(connectionId, tracked, memoBorrowedEnsure(this.#gatewayEnsure, connectionId, source, (id) => this.ensure(id), () => super.ensure(connectionId), () => dropConnection(this.kernel.root, connectionId)));
875
- }
876
- #loggedEnsure(connectionId, tracked, pending) {
877
- if (tracked)
878
- return pending;
879
- step(`ensure ${connectionId}`);
880
- void pending.then(() => step(`ensure ${connectionId} ready`), (error) => fail(`ensure ${connectionId}`, error));
881
- return pending;
882
- }
883
- attachConnection(connectionId, instance) {
884
- const kernel = this.sessionDaemon.kernel;
885
- instance.bindSettingsWriter((patch) => {
886
- kernel.adjustConfig((config) => {
887
- if (!Object.hasOwn(config.connections, connectionId))
888
- return;
889
- const row = config.connections[connectionId];
890
- row.settings = { ...row.settings, ...patch };
891
- });
892
- });
893
- }
894
- connectionSecrets(connectionId) {
895
- const root = this.sessionDaemon.kernel.root;
896
- const ownerId = () => secretsConnectionId(this.kernel.root, connectionId);
897
- return {
898
- get: (field) => readConnectionSecrets(root, ownerId())[field],
899
- all: () => readConnectionSecrets(root, ownerId()),
900
- replace: (secrets) => {
901
- writeConnectionSecrets(root, ownerId(), { ...secrets });
902
- },
903
- };
904
- }
905
- /**
906
- * Remainder / compact `.prompt` / turn port. Interns the row, then
907
- * `connection.complete` → `handle(round)`. `KernelDaemon` has no `complete()`.
908
- */
909
- complete(req) {
910
- return this.#flights.complete(req);
911
- }
912
- /**
913
- * Open completions honor will not start. `startedAtMs` means the attempt
914
- * already began. Tape completions belong to the turn resolution strategy. Only isolated
915
- * completions use this driver, so there is one dispatch owner per node.
916
- */
917
- #openUnstartedCompletions() {
918
- if (!this.activated || this.#closed)
919
- return [];
920
- const open = [];
921
- for (const node of this.reachableJobs) {
922
- if (!(node instanceof CompletionJob))
923
- continue;
924
- const pew = node.expectation;
925
- if (pew.isDone || pew.startedAtMs != null)
926
- continue;
927
- const parent = node.parent;
928
- if (parent instanceof SessionTurn && (parent.queue.includes(node) || parent.doneWork.includes(node)))
929
- continue;
930
- open.push(node);
1202
+ /** Honor still running after last-seat park: on-wire MCP or an unsealed eval bag. */
1203
+ get holding() {
1204
+ return this.#holdingDispatchedMcp || this.#holdingEval || this.#holdingCallbacks;
931
1205
  }
932
- return open;
933
- }
934
- #stopCompletions(reason) {
935
- this.#disposeCompletions();
936
- this.#flights.abortAll(reason);
937
- }
938
- async #driveCompletion(req) {
939
- const owner = req.round.parent;
940
- if (owner instanceof CompletionJob) {
941
- if (owner.orchestrationResult == null) {
942
- runInAction(() => {
943
- owner.orchestrationResult = true;
944
- });
945
- }
946
- const intent = await this.#gateIntent(owner, req.signal ?? new AbortController().signal);
947
- if (!intent.dispatch) {
948
- if (intent.persistFailed && req.round.outcome == null) {
949
- req.round.fail("crash", INTENT_NOT_PERSISTED, "null");
1206
+ get #callbackPending() { return _private_get_callbackPending_descriptor.get.call(this); }
1207
+ async #waitEvalAdmission(signal) {
1208
+ const held = this.sessionDaemon.evalAdmissionHeld;
1209
+ const ready = () => !held.get() && !this.sessionDaemon.sessionRuntime.admissionHeld;
1210
+ return ready() || (await waitUntil(ready, signal)) === "settled";
1211
+ }
1212
+ /**
1213
+ * Pause interpretation. Open Calls and agentCallbacks stay. Abort reason
1214
+ * `"park"` so Call / program / turn handlers do not seal.
1215
+ *
1216
+ * ONE SETTLEMENT. `tools/call` already on the wire is not aborted and not
1217
+ * started again — a second dispatch would hide whether the mutation landed.
1218
+ * Park waits for that settlement. Undispatched MCP is aborted without a
1219
+ * cancel receipt.
1220
+ *
1221
+ * Live eval bags are a wrap hold, not a mutation hold: last-seat drop must
1222
+ * not mint a second honor on the unsealed program. On-wire MCP does not
1223
+ * keep `interpreting` true.
1224
+ *
1225
+ * A held program keeps calling tools, and a tool is licensed and run by the
1226
+ * executors that a stop disposes. So the stop waits for the programs
1227
+ * instead of leaving them parked on a leaf nobody will run: until the last
1228
+ * live program settles this wrap keeps interpreting (and says so), then
1229
+ * parks as usual. The round a park interrupts is never ended: it is the
1230
+ * resumed turn that opens a fresh one.
1231
+ */
1232
+ park() {
1233
+ if (this.#closed)
1234
+ return Promise.resolve();
1235
+ if (liveEvalWork(this.session.root).size === 0 && !this.#callbackPending)
1236
+ return this.#parkNow();
1237
+ this.#deferredPark ??= (async () => {
1238
+ this.#holdingEval = liveEvalWork(this.session.root).size > 0;
1239
+ this.#holdingCallbacks = this.#callbackPending;
1240
+ try {
1241
+ await waitUntil(() => this.#closed || (liveEvalWork(this.session.root).size === 0 && !this.#callbackPending), this.sessionDaemon.sessionRuntime.ownershipSignal);
1242
+ }
1243
+ finally {
1244
+ this.#holdingEval = false;
1245
+ this.#holdingCallbacks = false;
950
1246
  }
951
- throw new Error(intent.persistFailed ? INTENT_NOT_PERSISTED : "completion cancelled");
1247
+ await this.#parkNow();
1248
+ })();
1249
+ return this.#deferredPark;
1250
+ }
1251
+ #deferredPark = null;
1252
+ #parkNow() {
1253
+ if (!this.#stop("park"))
1254
+ return Promise.resolve();
1255
+ if (!this.canApplyResults) {
1256
+ this.#release();
1257
+ return Promise.resolve();
1258
+ }
1259
+ const hold = this.#abortParkableWork();
1260
+ if (!hold.mcpPending && !hold.evalPending) {
1261
+ this.#abortParkableWork();
1262
+ this.#release();
1263
+ return Promise.resolve();
952
1264
  }
1265
+ this.#holdingDispatchedMcp = hold.mcpPending;
1266
+ this.#holdingEval = hold.evalPending;
1267
+ return when(() => dispatchedMcpSettled(this.session.root) && liveEvalWork(this.session.root).size === 0).then(() => {
1268
+ this.#holdingDispatchedMcp = false;
1269
+ this.#holdingEval = false;
1270
+ this.#abortParkableWork();
1271
+ this.#release();
1272
+ return undefined;
1273
+ });
953
1274
  }
954
- // The retry budget is read once, here, and recorded on the round. A live setting change
955
- // does not reach a round that is already retrying.
956
- req.round.stampRetryLimits({
957
- maxMs: this.kernel.root.retryMaxMs,
958
- maxAttempts: this.kernel.root.retryMaxAttempts,
959
- });
960
- const started = Date.now();
961
- const id = req.round.uuid;
962
- trace("harness:complete", `start ${id} connection=${req.connectionId} model=${req.model} isolated=${req.round.completionMode === "isolated"}`);
963
- this.originCompletions.add(req.round);
964
- try {
965
- const result = await internedComplete(this, req);
966
- trace("harness:complete", `done ${id} ms=${Date.now() - started} chars=${result.text.length}`);
967
- return result;
1275
+ /** Close the wrap to new work. Park and dispose share it; false once closed. */
1276
+ #stop(cause) {
1277
+ if (this.#closed)
1278
+ return false;
1279
+ this.#closed = true;
1280
+ this.#disposeLive();
1281
+ this.#disposeConnections();
1282
+ this.#disposeBoundWire();
1283
+ this.#disposePromptQueue();
1284
+ this.#disposeAutoAllow();
1285
+ this.#disposeDesk();
1286
+ // A park pauses the round it interrupts; only a dispose ends it.
1287
+ this.#stopCompletions(cause);
1288
+ this.stopAdmission();
1289
+ this.#behavior.stopAdmission();
1290
+ this.#closeScope();
1291
+ return true;
968
1292
  }
969
- catch (error) {
970
- trace("harness:complete", `fail ${id} ms=${Date.now() - started} ${error instanceof Error ? error.message : String(error)}`);
971
- if (error instanceof ConnectionMiss)
972
- this.originCompletions.delete(req.round);
973
- throw error;
1293
+ /** Release what the wrap holds, once its last held work settled. */
1294
+ #release() {
1295
+ this.#behavior[Symbol.dispose]();
1296
+ super[Symbol.dispose]();
974
1297
  }
975
- }
976
- /**
977
- * A guard or body threw. The job's joiners (the turn's `joinReleased`, a
978
- * program's leaf wait) read the receipt, so the model sees the error as
979
- * the tool result and the turn continues instead of waiting forever.
980
- */
981
- stampCrash(job, phase, detail) {
982
- const text = `${phase === "license" ? "license crashed" : "tool crashed"}: ${detail === "" ? "unknown error" : detail}`;
983
- trace("harness:crash", `${job.__type__} ${phase} ${detail}`);
984
- if (job instanceof ToolCallJob)
985
- stampUnentered(job, { ok: false, text });
986
- else if (job instanceof ProgrammaticJob)
987
- job.outcomeSnap ??= { ok: false, text };
988
- }
989
- shouldHonor(node) {
990
- if (!super.shouldHonor(node))
991
- return false;
992
- if (node instanceof McpToolExpectation)
993
- return node.startedAtMs != null || node.dispatched;
994
- if (node instanceof CompletionExpectation)
995
- return node.startedAtMs != null;
996
- return true;
997
- }
998
- async loadDriverModule(driver) {
999
- return this.sessionDaemon.kernel.runtime.loadDriver(driver);
1000
- }
1001
- #holdingDispatchedMcp = false;
1002
- #holdingEval = false;
1003
- get holdingEval() {
1004
- return this.#holdingEval;
1005
- }
1006
- /** Honor still running after last-seat park: on-wire MCP or an unsealed eval bag. */
1007
- get holding() {
1008
- return this.#holdingDispatchedMcp || this.#holdingEval;
1009
- }
1010
- async #waitEvalAdmission(signal) {
1011
- const held = this.sessionDaemon.evalAdmissionHeld;
1012
- return !held.get() || (await waitUntil(() => !held.get(), signal)) === "settled";
1013
- }
1014
- /**
1015
- * Pause interpretation. Open Calls and agentCallbacks stay. Abort reason
1016
- * `"park"` so Call / program / turn handlers do not seal.
1017
- *
1018
- * ONE SETTLEMENT. `tools/call` already on the wire is not aborted and not
1019
- * started again — a second dispatch would hide whether the mutation landed.
1020
- * Park waits for that settlement. Undispatched MCP is aborted without a
1021
- * cancel receipt.
1022
- *
1023
- * Live eval bags are a wrap hold, not a mutation hold: last-seat drop must
1024
- * not mint a second honor on the unsealed program. On-wire MCP does not
1025
- * keep `interpreting` true.
1026
- *
1027
- * A held program keeps calling tools, and a tool is licensed and run by the
1028
- * executors that a stop disposes. So the stop waits for the programs
1029
- * instead of leaving them parked on a leaf nobody will run: until the last
1030
- * live program settles this wrap keeps interpreting (and says so), then
1031
- * parks as usual. The round a park interrupts is never ended: it is the
1032
- * resumed turn that opens a fresh one.
1033
- */
1034
- park() {
1035
- if (this.#closed)
1036
- return Promise.resolve();
1037
- if (liveEvalWork(this.session.root).size === 0)
1038
- return this.#parkNow();
1039
- this.#deferredPark ??= (async () => {
1040
- this.#holdingEval = true;
1041
- try {
1042
- await when(() => this.#closed || liveEvalWork(this.session.root).size === 0);
1043
- }
1044
- finally {
1045
- this.#holdingEval = false;
1298
+ #abortParkableWork() {
1299
+ return abortParkableWork(this.session.root, {
1300
+ inFlightJobs: () => this.inFlightJobs(),
1301
+ abortJob: (job, reason) => this.abortJob(job, reason),
1302
+ abortExpectation: (expectation, reason) => {
1303
+ this.actorOf(expectation)?.abort(reason);
1304
+ },
1305
+ });
1306
+ }
1307
+ [(_private_get_callbackPending_decorators = [computed], Symbol.dispose)]() {
1308
+ if (!this.#stop())
1309
+ return;
1310
+ if (this.canApplyResults) {
1311
+ for (const job of this.inFlightJobs()) {
1312
+ if (job instanceof SessionTurn) {
1313
+ failLeftoverApprovables(job, (leaf) => this.#abortLeaf(leaf), this.sessionDaemon.kernel.catalog);
1314
+ }
1315
+ }
1046
1316
  }
1047
- await this.#parkNow();
1048
- })();
1049
- return this.#deferredPark;
1050
- }
1051
- #deferredPark = null;
1052
- #parkNow() {
1053
- if (!this.#stop("park"))
1054
- return Promise.resolve();
1055
- const hold = this.#abortParkableWork();
1056
- if (!hold.mcpPending && !hold.evalPending) {
1057
- this.#abortParkableWork();
1058
1317
  this.#release();
1059
- return Promise.resolve();
1060
1318
  }
1061
- this.#holdingDispatchedMcp = hold.mcpPending;
1062
- this.#holdingEval = hold.evalPending;
1063
- return when(() => dispatchedMcpSettled(this.session.root) && liveEvalWork(this.session.root).size === 0).then(() => {
1064
- this.#holdingDispatchedMcp = false;
1065
- this.#holdingEval = false;
1066
- this.#abortParkableWork();
1067
- this.#release();
1068
- return undefined;
1069
- });
1070
- }
1071
- /** Close the wrap to new work. Park and dispose share it; false once closed. */
1072
- #stop(cause) {
1073
- if (this.#closed)
1074
- return false;
1075
- this.#closed = true;
1076
- this.#disposeLive();
1077
- this.#disposeConnections();
1078
- this.#disposeBoundWire();
1079
- this.#disposePromptQueue();
1080
- this.#disposeAutoAllow();
1081
- this.#disposeDesk();
1082
- // A park pauses the round it interrupts; only a dispose ends it.
1083
- this.#stopCompletions(cause);
1084
- this.stopAdmission();
1085
- scopes.get(this.session.root).close();
1086
- return true;
1087
- }
1088
- /** Release what the wrap holds, once its last held work settled. */
1089
- #release() {
1090
- this.#behavior[Symbol.dispose]();
1091
- super[Symbol.dispose]();
1092
- }
1093
- #abortParkableWork() {
1094
- return abortParkableWork(this.session.root, {
1095
- inFlightJobs: () => this.inFlightJobs(),
1096
- abortJob: (job, reason) => this.abortJob(job, reason),
1097
- abortExpectation: (expectation, reason) => {
1098
- this.actorOf(expectation)?.abort(reason);
1099
- },
1100
- });
1101
- }
1102
- [Symbol.dispose]() {
1103
- if (!this.#stop())
1104
- return;
1105
- for (const job of this.inFlightJobs()) {
1106
- if (job instanceof SessionTurn) {
1107
- failLeftoverApprovables(job, (leaf) => this.#abortLeaf(leaf), this.sessionDaemon.kernel.catalog);
1319
+ #compactHost(turn, session, connectionId, model) {
1320
+ return {
1321
+ turn,
1322
+ session,
1323
+ compactKeepTurns: this.kernel.root.compactKeepTurns,
1324
+ compactStrategy: this.kernel.root.compactStrategy,
1325
+ kernelFsRoot: this.sessionDaemon.kernel.root,
1326
+ connectionId,
1327
+ model,
1328
+ complete: (req) => this.complete(req),
1329
+ loadProgram: (spec, root) => loadCompactProgram(spec, root),
1330
+ checkpoint: () => this.sessionDaemon.sessionRuntime.flush().then(() => undefined),
1331
+ log: (message) => step(message),
1332
+ fail: (label, error) => fail(label, error),
1333
+ };
1334
+ }
1335
+ cancelOwned(turn) {
1336
+ const catalog = this.sessionDaemon.kernel.catalog;
1337
+ turn.cancelRequested = true;
1338
+ for (const node of walkWork(turn)) {
1339
+ if (node instanceof CallJob && !node.completed) {
1340
+ if (shouldSkipTurnCancel(node, catalog))
1341
+ continue;
1342
+ completeCallExceptionally(node, this.#agentHost(), "cancelled");
1343
+ }
1344
+ }
1345
+ failLeftoverApprovables(turn, (leaf) => this.#abortLeaf(leaf), catalog);
1346
+ for (const node of walkWork(turn)) {
1347
+ if (node instanceof ProgrammaticJob && !node.completed) {
1348
+ if (node.orchestrated)
1349
+ this.abortJob(node);
1350
+ else {
1351
+ node.outcomeSnap ??= { ok: false, text: "cancelled" };
1352
+ node.cancelled = true;
1353
+ node.markComplete();
1354
+ }
1355
+ }
1356
+ if (node instanceof WorkExpectation && !node.isDone)
1357
+ this.#terminatePew(node);
1358
+ }
1359
+ let inFlight = false;
1360
+ for (const job of this.inFlightJobs()) {
1361
+ if (job === turn) {
1362
+ inFlight = true;
1363
+ break;
1364
+ }
1365
+ }
1366
+ this.abortJob(turn);
1367
+ if (!inFlight) {
1368
+ // The executor is not inside this turn, so its finally will not clear `inLoop`.
1369
+ closeTurn(turn, { kind: "cancel" }, this.#closeOptions());
1108
1370
  }
1109
1371
  }
1110
- this.#release();
1111
- }
1112
- #compactHost(turn, session, connectionId, model) {
1113
- return {
1114
- turn,
1115
- session,
1116
- compactKeepTurns: this.kernel.root.compactKeepTurns,
1117
- compactStrategy: this.kernel.root.compactStrategy,
1118
- kernelFsRoot: this.sessionDaemon.kernel.root,
1119
- connectionId,
1120
- model,
1121
- complete: (req) => this.complete(req),
1122
- loadProgram: (spec, root) => loadCompactProgram(spec, root),
1123
- log: (message) => step(message),
1124
- fail: (label, error) => fail(label, error),
1125
- };
1126
- }
1127
- cancelOwned(turn) {
1128
- const catalog = this.sessionDaemon.kernel.catalog;
1129
- turn.cancelRequested = true;
1130
- for (const node of walkWork(turn)) {
1131
- if (node instanceof CallJob && !node.completed) {
1132
- if (shouldSkipTurnCancel(node, catalog))
1372
+ /** What every turn close needs from this wrap: the abort of an in-flight body, the actor-aware end, the inbox. */
1373
+ #closeOptions() {
1374
+ return {
1375
+ catalog: this.sessionDaemon.kernel.catalog,
1376
+ abort: (job) => (job instanceof ApprovableJob ? this.#abortLeaf(job) : this.abortJob(job)),
1377
+ terminate: (expectation) => this.#terminatePew(expectation),
1378
+ releaseCall: (call) => {
1379
+ if (!call.completed)
1380
+ completeCallExceptionally(call, this.#agentHost(), "cancelled");
1381
+ },
1382
+ session: this.session.root,
1383
+ };
1384
+ }
1385
+ /**
1386
+ * Release the caller's open Call when a worker turn closes without answering:
1387
+ * on any non-cancelled completion failure, turn error, or one-shot worker loop exit.
1388
+ * The turn is already closed, so force-stop does not abort this executor.
1389
+ */
1390
+ #releaseUnansweredCall(turn) {
1391
+ const requestId = turn.agentRequestId.trim();
1392
+ if (requestId === "")
1393
+ return;
1394
+ const call = loadOpenCall(this.sessionDaemon.kernel, this.session.root, requestId);
1395
+ if (call == null || call.completed)
1396
+ return;
1397
+ let detail = "";
1398
+ let hasCompletion = false;
1399
+ let hasOpenChildCall = false;
1400
+ let stopDetail = "";
1401
+ for (const node of walkWork(turn)) {
1402
+ if (node instanceof CallJob && !node.completed) {
1403
+ hasOpenChildCall = true;
1404
+ }
1405
+ if (node instanceof ReACTChunk && node.decision?.kind === "blocked") {
1406
+ const reason = node.decision.detail?.trim() || "";
1407
+ if (reason !== "")
1408
+ stopDetail = reason;
1409
+ }
1410
+ const pew = node instanceof CompletionExpectation ? node : tapeCompletion(node);
1411
+ if (pew == null || pew.startedAtMs == null)
1133
1412
  continue;
1134
- completeCallExceptionally(node, this.#agentHost(), "cancelled");
1413
+ hasCompletion = true;
1414
+ if (pew.outcome !== "failed" || pew.endCause === "cancel")
1415
+ continue;
1416
+ const text = pew.endDetail?.trim() || pew.failReason?.trim() || "model round failed";
1417
+ detail = text;
1418
+ break;
1135
1419
  }
1420
+ if (detail === "" &&
1421
+ hasCompletion &&
1422
+ !hasOpenChildCall &&
1423
+ !this.session.root.agent.spec.keepAlive &&
1424
+ this.session.root.agent.spec instanceof AskAgentSpec) {
1425
+ detail = turn.broke.trim() || stopDetail || "worker turn ended without calling agent__respond";
1426
+ }
1427
+ if (detail === "")
1428
+ return;
1429
+ completeCallExceptionally(call, this.#agentHost(), "error", detail);
1136
1430
  }
1137
- failLeftoverApprovables(turn, (leaf) => this.#abortLeaf(leaf), catalog);
1138
- for (const node of walkWork(turn)) {
1139
- if (node instanceof ProgrammaticJob && !node.completed) {
1140
- if (node.orchestrated)
1141
- this.abortJob(node);
1142
- else {
1143
- node.outcomeSnap ??= { ok: false, text: "cancelled" };
1144
- node.cancelled = true;
1145
- node.markComplete();
1431
+ /**
1432
+ * In-flight BODY abort. `abortJob` does not stop a worker session — AgentCall
1433
+ * must complete exceptionally. MCP/web terminate PEW.
1434
+ */
1435
+ #abortLeaf(leaf, reason) {
1436
+ this.abortJob(leaf, reason);
1437
+ if (leaf instanceof CallJob) {
1438
+ if (!leaf.completed) {
1439
+ completeCallExceptionally(leaf, this.#agentHost(), "cancelled", cancelOutcomeText(reason));
1146
1440
  }
1441
+ return;
1147
1442
  }
1148
- if (node instanceof WorkExpectation && !node.isDone)
1149
- this.#terminatePew(node);
1150
- }
1151
- let inFlight = false;
1152
- for (const job of this.inFlightJobs()) {
1153
- if (job === turn) {
1154
- inFlight = true;
1155
- break;
1443
+ if (leaf instanceof MCPToolJob || leaf instanceof WebSearchToolJob || leaf instanceof WebExtractToolJob) {
1444
+ leaf.outcomeSnap ??= { ok: false, text: cancelOutcomeText(reason) };
1445
+ const pew = leaf.expectation;
1446
+ if (!pew.isDone)
1447
+ this.#terminatePew(pew);
1156
1448
  }
1157
1449
  }
1158
- this.abortJob(turn);
1159
- if (!inFlight) {
1160
- // The executor is not inside this turn, so its finally will not clear `inLoop`.
1161
- closeTurn(turn, { kind: "cancel" }, this.#closeOptions());
1450
+ #abortWork(job) {
1451
+ if (job instanceof ApprovableJob)
1452
+ this.#abortLeaf(job);
1453
+ else
1454
+ this.abortJob(job);
1162
1455
  }
1163
- }
1164
- /** What every turn close needs from this wrap: the abort of an in-flight body, the actor-aware end, the inbox. */
1165
- #closeOptions() {
1166
- return {
1167
- catalog: this.sessionDaemon.kernel.catalog,
1168
- abort: (job) => (job instanceof ApprovableJob ? this.#abortLeaf(job) : this.abortJob(job)),
1169
- terminate: (expectation) => this.#terminatePew(expectation),
1170
- releaseCall: (call) => {
1171
- if (!call.completed)
1172
- completeCallExceptionally(call, this.#agentHost(), "cancelled");
1173
- },
1174
- session: this.session.root,
1175
- };
1176
- }
1177
- /**
1178
- * Stream stall failed this turn's completion. A keepAlive worker can still
1179
- * respond after any other close, and eval timeout cancels the call itself.
1180
- * The turn is already closed, so force-stop does not abort this executor.
1181
- */
1182
- #releaseUnansweredCall(turn) {
1183
- const requestId = turn.agentRequestId.trim();
1184
- if (requestId === "")
1185
- return;
1186
- const call = loadOpenCall(this.sessionDaemon.kernel, this.session.root, requestId);
1187
- if (call == null || call.completed)
1188
- return;
1189
- let detail = "";
1190
- for (const node of walkWork(turn)) {
1191
- const pew = node instanceof CompletionExpectation ? node : tapeCompletion(node);
1192
- if (pew == null)
1193
- continue;
1194
- if (pew.outcome !== "failed")
1195
- continue;
1196
- const text = pew.endDetail?.trim() || pew.failReason?.trim() || "";
1197
- if (!text.startsWith("stream stalled after "))
1198
- continue;
1199
- detail = text;
1200
- break;
1456
+ #terminatePew(expectation) {
1457
+ const actor = this.actorOf(expectation);
1458
+ if (actor !== undefined)
1459
+ actor.admitTermination();
1460
+ if (!expectation.isDone)
1461
+ expectation.fail("cancel", "terminate", "null");
1201
1462
  }
1202
- if (detail === "")
1203
- return;
1204
- completeCallExceptionally(call, this.#agentHost(), "error", detail);
1205
- }
1206
- /**
1207
- * In-flight BODY abort. `abortJob` does not stop a worker session — AgentCall
1208
- * must complete exceptionally. MCP/web terminate PEW.
1209
- */
1210
- #abortLeaf(leaf, reason) {
1211
- this.abortJob(leaf, reason);
1212
- if (leaf instanceof CallJob) {
1213
- if (!leaf.completed) {
1214
- completeCallExceptionally(leaf, this.#agentHost(), "cancelled", cancelOutcomeText(reason));
1463
+ #configuredModel(connectionId) {
1464
+ const spec = this.kernel.root.connections[connectionId];
1465
+ if (spec == null)
1466
+ return "";
1467
+ const configured = typeof spec.configuration.model === "string" ? spec.configuration.model.trim() : "";
1468
+ if (configured !== "")
1469
+ return configured;
1470
+ const activeConnection = (this.session.root.kvs.connection || this.kernel.root.kvs.connection || "").trim();
1471
+ if (connectionId === activeConnection) {
1472
+ const activeModel = (this.session.root.kvs.model || this.kernel.root.kvs.model || "").trim();
1473
+ if (activeModel !== "")
1474
+ return activeModel;
1475
+ }
1476
+ const liveModels = spec.live?.models;
1477
+ if (Array.isArray(liveModels)) {
1478
+ const first = liveModels.find((id) => typeof id === "string" && id.trim() !== "");
1479
+ if (typeof first === "string")
1480
+ return first.trim();
1215
1481
  }
1216
- return;
1217
- }
1218
- if (leaf instanceof MCPToolJob || leaf instanceof WebSearchToolJob || leaf instanceof WebExtractToolJob) {
1219
- leaf.outcomeSnap ??= { ok: false, text: cancelOutcomeText(reason) };
1220
- const pew = leaf.expectation;
1221
- if (!pew.isDone)
1222
- this.#terminatePew(pew);
1223
- }
1224
- }
1225
- #abortWork(job) {
1226
- if (job instanceof ApprovableJob)
1227
- this.#abortLeaf(job);
1228
- else
1229
- this.abortJob(job);
1230
- }
1231
- #terminatePew(expectation) {
1232
- const actor = this.actorOf(expectation);
1233
- if (actor !== undefined)
1234
- actor.admitTermination();
1235
- if (!expectation.isDone)
1236
- expectation.fail("cancel", "terminate", "null");
1237
- }
1238
- #configuredModel(connectionId) {
1239
- const spec = this.kernel.root.connections[connectionId];
1240
- if (spec == null)
1241
1482
  return "";
1242
- const configured = typeof spec.configuration.model === "string" ? spec.configuration.model.trim() : "";
1243
- if (configured !== "")
1244
- return configured;
1245
- const activeConnection = (this.session.root.kvs.connection || this.kernel.root.kvs.connection || "").trim();
1246
- if (connectionId === activeConnection) {
1247
- const activeModel = (this.session.root.kvs.model || this.kernel.root.kvs.model || "").trim();
1248
- if (activeModel !== "")
1249
- return activeModel;
1250
1483
  }
1251
- const liveModels = spec.live?.models;
1252
- if (Array.isArray(liveModels)) {
1253
- const first = liveModels.find((id) => typeof id === "string" && id.trim() !== "");
1254
- if (typeof first === "string")
1255
- return first.trim();
1484
+ async #reActLoopIntentDetectedTrigger(turn, pew, signal) {
1485
+ if (!reActLoopIntentDetectedTriggerFor(this.kernel.root.reActLoopIntentDetectedTrigger, pew.connectionId, pew.model))
1486
+ return { verdict: null, status: "disabled", detail: "Continuation judge is disabled." };
1487
+ const wire = internJudgeWire(this.session.root.kvs, this.kernel.root.kvs, (id) => this.#configuredModel(id));
1488
+ if (wire == null)
1489
+ return { verdict: null, status: "unconfigured", detail: "No continuation judge is configured." };
1490
+ return assessTextOnlyContinueIntern({
1491
+ speech: pew.responseText,
1492
+ wire,
1493
+ haltKeys: this.#reActLoopIntentDetectedTriggerHaltKeys,
1494
+ home: turn,
1495
+ complete: (req) => this.complete(req),
1496
+ signal,
1497
+ });
1256
1498
  }
1257
- return "";
1258
- }
1259
- async #reActLoopIntentDetectedTrigger(turn, pew, signal) {
1260
- if (internContinueStreak(turn) >= RE_ACT_LOOP_INTENT_DETECTED_TRIGGER_STREAK_MAX)
1261
- return "seal";
1262
- if (!reActLoopIntentDetectedTriggerFor(this.kernel.root.reActLoopIntentDetectedTrigger, pew.connectionId, pew.model))
1263
- return "seal";
1264
- const wire = internJudgeWire(this.session.root.kvs, this.kernel.root.kvs, (id) => this.#configuredModel(id));
1265
- if (wire == null)
1266
- return "seal";
1267
- return runTextOnlyContinueIntern({
1268
- speech: pew.responseText,
1269
- wire,
1270
- haltKeys: this.#reActLoopIntentDetectedTriggerHaltKeys,
1271
- home: turn,
1272
- complete: (req) => this.complete(req),
1273
- signal,
1274
- });
1275
- }
1276
- }
1499
+ };
1500
+ })();
1501
+ export { SessionOrchestrator };
1277
1502
  function completionRequest(pew) {
1278
1503
  return {
1279
1504
  connectionId: pew.connectionId,