@senpi/runtime 3.0.173 → 3.0.181

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 (293) hide show
  1. package/dist/actions/close-position/close-position.action.d.ts.map +1 -1
  2. package/dist/actions/close-position/close-position.action.js +48 -3
  3. package/dist/actions/close-position/close-position.action.js.map +1 -1
  4. package/dist/actions/llm-decision/openclaw-llm-decision.d.ts +8 -0
  5. package/dist/actions/llm-decision/openclaw-llm-decision.d.ts.map +1 -1
  6. package/dist/actions/llm-decision/openclaw-llm-decision.js +56 -1
  7. package/dist/actions/llm-decision/openclaw-llm-decision.js.map +1 -1
  8. package/dist/actions/open-position/open-position.action.d.ts.map +1 -1
  9. package/dist/actions/open-position/open-position.action.js +42 -6
  10. package/dist/actions/open-position/open-position.action.js.map +1 -1
  11. package/dist/actions/open-position/resolve-margin.d.ts +1 -1
  12. package/dist/actions/open-position/resolve-margin.d.ts.map +1 -1
  13. package/dist/actions/open-position/resolve-margin.js +7 -0
  14. package/dist/actions/open-position/resolve-margin.js.map +1 -1
  15. package/dist/approvals/approval-store.d.ts +37 -7
  16. package/dist/approvals/approval-store.d.ts.map +1 -1
  17. package/dist/approvals/approval-store.js +127 -23
  18. package/dist/approvals/approval-store.js.map +1 -1
  19. package/dist/approvals/manifest.d.ts.map +1 -1
  20. package/dist/approvals/manifest.js +0 -5
  21. package/dist/approvals/manifest.js.map +1 -1
  22. package/dist/approvals/telemetry.d.ts +27 -3
  23. package/dist/approvals/telemetry.d.ts.map +1 -1
  24. package/dist/approvals/telemetry.js +8 -4
  25. package/dist/approvals/telemetry.js.map +1 -1
  26. package/dist/approvals/tool-approval-gate.d.ts +79 -0
  27. package/dist/approvals/tool-approval-gate.d.ts.map +1 -1
  28. package/dist/approvals/tool-approval-gate.js +184 -21
  29. package/dist/approvals/tool-approval-gate.js.map +1 -1
  30. package/dist/approvals/tool-approvals-manifest.json +0 -2
  31. package/dist/approvals/turn-context.d.ts +271 -0
  32. package/dist/approvals/turn-context.d.ts.map +1 -0
  33. package/dist/approvals/turn-context.js +551 -0
  34. package/dist/approvals/turn-context.js.map +1 -0
  35. package/dist/cli/guide-commands.d.ts.map +1 -1
  36. package/dist/cli/guide-commands.js +43 -8
  37. package/dist/cli/guide-commands.js.map +1 -1
  38. package/dist/cli/senpi-commands.d.ts.map +1 -1
  39. package/dist/cli/senpi-commands.js +91 -12
  40. package/dist/cli/senpi-commands.js.map +1 -1
  41. package/dist/constants/dsl/index.d.ts +2 -0
  42. package/dist/constants/dsl/index.d.ts.map +1 -1
  43. package/dist/constants/dsl/index.js +2 -0
  44. package/dist/constants/dsl/index.js.map +1 -1
  45. package/dist/constants/order-types.d.ts.map +1 -1
  46. package/dist/constants/order-types.js +4 -1
  47. package/dist/constants/order-types.js.map +1 -1
  48. package/dist/deploy/job.d.ts +32 -0
  49. package/dist/deploy/job.d.ts.map +1 -1
  50. package/dist/deploy/job.js +72 -7
  51. package/dist/deploy/job.js.map +1 -1
  52. package/dist/deploy/messages.d.ts +15 -1
  53. package/dist/deploy/messages.d.ts.map +1 -1
  54. package/dist/deploy/messages.js +78 -0
  55. package/dist/deploy/messages.js.map +1 -1
  56. package/dist/deploy/orchestrator.d.ts +11 -1
  57. package/dist/deploy/orchestrator.d.ts.map +1 -1
  58. package/dist/deploy/orchestrator.js +111 -15
  59. package/dist/deploy/orchestrator.js.map +1 -1
  60. package/dist/deploy/register.d.ts +18 -0
  61. package/dist/deploy/register.d.ts.map +1 -1
  62. package/dist/deploy/register.js +93 -2
  63. package/dist/deploy/register.js.map +1 -1
  64. package/dist/dsl/engine/exchange-stop.d.ts +21 -1
  65. package/dist/dsl/engine/exchange-stop.d.ts.map +1 -1
  66. package/dist/dsl/engine/exchange-stop.js +27 -1
  67. package/dist/dsl/engine/exchange-stop.js.map +1 -1
  68. package/dist/dsl/engine/floors.d.ts +2 -1
  69. package/dist/dsl/engine/floors.d.ts.map +1 -1
  70. package/dist/dsl/engine/floors.js +2 -1
  71. package/dist/dsl/engine/floors.js.map +1 -1
  72. package/dist/dsl/engine/index.d.ts +1 -1
  73. package/dist/dsl/engine/index.d.ts.map +1 -1
  74. package/dist/dsl/engine/index.js +1 -1
  75. package/dist/dsl/engine/index.js.map +1 -1
  76. package/dist/dsl/engine/tick.d.ts.map +1 -1
  77. package/dist/dsl/engine/tick.js +4 -3
  78. package/dist/dsl/engine/tick.js.map +1 -1
  79. package/dist/dsl/events/handlers.d.ts +7 -3
  80. package/dist/dsl/events/handlers.d.ts.map +1 -1
  81. package/dist/dsl/events/handlers.js +10 -4
  82. package/dist/dsl/events/handlers.js.map +1 -1
  83. package/dist/dsl/events/initial-state.d.ts.map +1 -1
  84. package/dist/dsl/events/initial-state.js +2 -0
  85. package/dist/dsl/events/initial-state.js.map +1 -1
  86. package/dist/dsl/monitor/exchange-sync.d.ts +1 -0
  87. package/dist/dsl/monitor/exchange-sync.d.ts.map +1 -1
  88. package/dist/dsl/monitor/exchange-sync.js +7 -14
  89. package/dist/dsl/monitor/exchange-sync.js.map +1 -1
  90. package/dist/dsl/monitor/process-one-position.d.ts.map +1 -1
  91. package/dist/dsl/monitor/process-one-position.js +108 -17
  92. package/dist/dsl/monitor/process-one-position.js.map +1 -1
  93. package/dist/dsl/notifications/formatter.d.ts +2 -1
  94. package/dist/dsl/notifications/formatter.d.ts.map +1 -1
  95. package/dist/dsl/notifications/formatter.js +18 -1
  96. package/dist/dsl/notifications/formatter.js.map +1 -1
  97. package/dist/health/types.d.ts +6 -1
  98. package/dist/health/types.d.ts.map +1 -1
  99. package/dist/health/types.js.map +1 -1
  100. package/dist/host/openclaw-host.d.ts +138 -0
  101. package/dist/host/openclaw-host.d.ts.map +1 -0
  102. package/dist/host/openclaw-host.js +177 -0
  103. package/dist/host/openclaw-host.js.map +1 -0
  104. package/dist/index.d.ts +46 -2
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +318 -38
  107. package/dist/index.js.map +1 -1
  108. package/dist/leverage/max-leverage-cache.d.ts +25 -0
  109. package/dist/leverage/max-leverage-cache.d.ts.map +1 -1
  110. package/dist/leverage/max-leverage-cache.js +77 -0
  111. package/dist/leverage/max-leverage-cache.js.map +1 -1
  112. package/dist/runtime/auto-update/coordinator.d.ts +203 -0
  113. package/dist/runtime/auto-update/coordinator.d.ts.map +1 -1
  114. package/dist/runtime/auto-update/coordinator.js +621 -12
  115. package/dist/runtime/auto-update/coordinator.js.map +1 -1
  116. package/dist/runtime/auto-update/gateway-restart.d.ts +60 -1
  117. package/dist/runtime/auto-update/gateway-restart.d.ts.map +1 -1
  118. package/dist/runtime/auto-update/gateway-restart.js +95 -21
  119. package/dist/runtime/auto-update/gateway-restart.js.map +1 -1
  120. package/dist/runtime/auto-update/idle-gate.d.ts +139 -0
  121. package/dist/runtime/auto-update/idle-gate.d.ts.map +1 -0
  122. package/dist/runtime/auto-update/idle-gate.js +217 -0
  123. package/dist/runtime/auto-update/idle-gate.js.map +1 -0
  124. package/dist/runtime/auto-update/install-results.d.ts +130 -0
  125. package/dist/runtime/auto-update/install-results.d.ts.map +1 -0
  126. package/dist/runtime/auto-update/install-results.js +264 -0
  127. package/dist/runtime/auto-update/install-results.js.map +1 -0
  128. package/dist/runtime/auto-update/money-call-tracker.d.ts +36 -0
  129. package/dist/runtime/auto-update/money-call-tracker.d.ts.map +1 -0
  130. package/dist/runtime/auto-update/money-call-tracker.js +129 -0
  131. package/dist/runtime/auto-update/money-call-tracker.js.map +1 -0
  132. package/dist/runtime/auto-update/register-idle-gate.d.ts +70 -0
  133. package/dist/runtime/auto-update/register-idle-gate.d.ts.map +1 -0
  134. package/dist/runtime/auto-update/register-idle-gate.js +123 -0
  135. package/dist/runtime/auto-update/register-idle-gate.js.map +1 -0
  136. package/dist/runtime/auto-update/run-update-command.d.ts +13 -0
  137. package/dist/runtime/auto-update/run-update-command.d.ts.map +1 -1
  138. package/dist/runtime/auto-update/run-update-command.js +18 -0
  139. package/dist/runtime/auto-update/run-update-command.js.map +1 -1
  140. package/dist/runtime/auto-update/state-store.d.ts +14 -1
  141. package/dist/runtime/auto-update/state-store.d.ts.map +1 -1
  142. package/dist/runtime/auto-update/state-store.js +37 -3
  143. package/dist/runtime/auto-update/state-store.js.map +1 -1
  144. package/dist/runtime/auto-update/types.d.ts +51 -0
  145. package/dist/runtime/auto-update/types.d.ts.map +1 -1
  146. package/dist/runtime/auto-update/types.js.map +1 -1
  147. package/dist/runtime/auto-update/update-marker.d.ts +95 -0
  148. package/dist/runtime/auto-update/update-marker.d.ts.map +1 -0
  149. package/dist/runtime/auto-update/update-marker.js +185 -0
  150. package/dist/runtime/auto-update/update-marker.js.map +1 -0
  151. package/dist/runtime/auto-update/update-schedule.d.ts +57 -0
  152. package/dist/runtime/auto-update/update-schedule.d.ts.map +1 -0
  153. package/dist/runtime/auto-update/update-schedule.js +75 -0
  154. package/dist/runtime/auto-update/update-schedule.js.map +1 -0
  155. package/dist/runtime/create-scanner-module.d.ts +1 -1
  156. package/dist/runtime/create-scanner-module.d.ts.map +1 -1
  157. package/dist/runtime/create-scanner-module.js +2 -1
  158. package/dist/runtime/create-scanner-module.js.map +1 -1
  159. package/dist/runtime/decision-errors.d.ts +7 -0
  160. package/dist/runtime/decision-errors.d.ts.map +1 -1
  161. package/dist/runtime/decision-errors.js +15 -0
  162. package/dist/runtime/decision-errors.js.map +1 -1
  163. package/dist/runtime/lifecycle-events.d.ts +1 -1
  164. package/dist/runtime/lifecycle-events.d.ts.map +1 -1
  165. package/dist/runtime/lifecycle-events.js +10 -1
  166. package/dist/runtime/lifecycle-events.js.map +1 -1
  167. package/dist/runtime/runtime-descriptor.d.ts +11 -2
  168. package/dist/runtime/runtime-descriptor.d.ts.map +1 -1
  169. package/dist/runtime/runtime-descriptor.js +11 -2
  170. package/dist/runtime/runtime-descriptor.js.map +1 -1
  171. package/dist/runtime/runtime.d.ts.map +1 -1
  172. package/dist/runtime/runtime.js +4 -3
  173. package/dist/runtime/runtime.js.map +1 -1
  174. package/dist/runtime/strategy-lifecycle.js +1 -1
  175. package/dist/runtime/strategy-lifecycle.js.map +1 -1
  176. package/dist/runtime/supervisor/long-running-supervisor.js +1 -1
  177. package/dist/runtime/supervisor/long-running-supervisor.js.map +1 -1
  178. package/dist/runtime-api/intake/acceptance-record.d.ts +17 -3
  179. package/dist/runtime-api/intake/acceptance-record.d.ts.map +1 -1
  180. package/dist/runtime-api/intake/acceptance-record.js +57 -32
  181. package/dist/runtime-api/intake/acceptance-record.js.map +1 -1
  182. package/dist/runtime-api/intake/intake-events.d.ts +6 -5
  183. package/dist/runtime-api/intake/intake-events.d.ts.map +1 -1
  184. package/dist/runtime-api/intake/intake-events.js +9 -3
  185. package/dist/runtime-api/intake/intake-events.js.map +1 -1
  186. package/dist/runtime-api/intake/process-signal-post.d.ts +1 -1
  187. package/dist/runtime-api/intake/process-signal-post.d.ts.map +1 -1
  188. package/dist/runtime-api/intake/process-signal-post.js +34 -31
  189. package/dist/runtime-api/intake/process-signal-post.js.map +1 -1
  190. package/dist/runtime-api/intake/scaffold-error-router.d.ts +8 -0
  191. package/dist/runtime-api/intake/scaffold-error-router.d.ts.map +1 -1
  192. package/dist/runtime-api/intake/scaffold-error-router.js +9 -0
  193. package/dist/runtime-api/intake/scaffold-error-router.js.map +1 -1
  194. package/dist/runtime-api/intake/types.d.ts +9 -0
  195. package/dist/runtime-api/intake/types.d.ts.map +1 -1
  196. package/dist/runtime-api/intake/types.js +5 -1
  197. package/dist/runtime-api/intake/types.js.map +1 -1
  198. package/dist/runtime-api/routes/signals.d.ts.map +1 -1
  199. package/dist/runtime-api/routes/signals.js +25 -1
  200. package/dist/runtime-api/routes/signals.js.map +1 -1
  201. package/dist/runtime-api/routes/signals.schema.d.ts +2 -0
  202. package/dist/runtime-api/routes/signals.schema.d.ts.map +1 -1
  203. package/dist/runtime-api/routes/signals.schema.js +4 -0
  204. package/dist/runtime-api/routes/signals.schema.js.map +1 -1
  205. package/dist/scanners/engine/data-providers.d.ts +6 -0
  206. package/dist/scanners/engine/data-providers.d.ts.map +1 -1
  207. package/dist/scanners/engine/data-providers.js.map +1 -1
  208. package/dist/scanners/engine/engine.d.ts +9 -0
  209. package/dist/scanners/engine/engine.d.ts.map +1 -1
  210. package/dist/scanners/engine/engine.js +29 -6
  211. package/dist/scanners/engine/engine.js.map +1 -1
  212. package/dist/scanners/external/scaffold/ctx.py +55 -1
  213. package/dist/scanners/external/scaffold/delivery.py +92 -28
  214. package/dist/scanners/external/scaffold/preflight.py +1 -1
  215. package/dist/scanners/external/scaffold/scaffold.py +75 -2
  216. package/dist/scanners/runtime-module.d.ts +2 -0
  217. package/dist/scanners/runtime-module.d.ts.map +1 -1
  218. package/dist/scanners/runtime-module.js +14 -0
  219. package/dist/scanners/runtime-module.js.map +1 -1
  220. package/dist/senpi/client.d.ts +36 -1
  221. package/dist/senpi/client.d.ts.map +1 -1
  222. package/dist/senpi/client.js +100 -8
  223. package/dist/senpi/client.js.map +1 -1
  224. package/dist/senpi/types.d.ts +54 -2
  225. package/dist/senpi/types.d.ts.map +1 -1
  226. package/dist/telemetry/metrics.d.ts +15 -1
  227. package/dist/telemetry/metrics.d.ts.map +1 -1
  228. package/dist/telemetry/metrics.js +25 -1
  229. package/dist/telemetry/metrics.js.map +1 -1
  230. package/dist/telemetry/otel-sdk.d.ts +12 -0
  231. package/dist/telemetry/otel-sdk.d.ts.map +1 -1
  232. package/dist/telemetry/otel-sdk.js +47 -1
  233. package/dist/telemetry/otel-sdk.js.map +1 -1
  234. package/dist/telemetry/spans/span-hooks.d.ts +18 -1
  235. package/dist/telemetry/spans/span-hooks.d.ts.map +1 -1
  236. package/dist/telemetry/spans/span-hooks.js +306 -96
  237. package/dist/telemetry/spans/span-hooks.js.map +1 -1
  238. package/dist/telemetry/tool-plane.d.ts +2 -0
  239. package/dist/telemetry/tool-plane.d.ts.map +1 -1
  240. package/dist/telemetry/tool-plane.js +1 -0
  241. package/dist/telemetry/tool-plane.js.map +1 -1
  242. package/dist/tool-surface/crib-cache.d.ts +21 -0
  243. package/dist/tool-surface/crib-cache.d.ts.map +1 -0
  244. package/dist/tool-surface/crib-cache.js +54 -0
  245. package/dist/tool-surface/crib-cache.js.map +1 -0
  246. package/dist/tool-surface/crib-hook.d.ts +78 -0
  247. package/dist/tool-surface/crib-hook.d.ts.map +1 -0
  248. package/dist/tool-surface/crib-hook.js +217 -0
  249. package/dist/tool-surface/crib-hook.js.map +1 -0
  250. package/dist/tool-surface/crib-sheet.d.ts +56 -0
  251. package/dist/tool-surface/crib-sheet.d.ts.map +1 -0
  252. package/dist/tool-surface/crib-sheet.js +129 -0
  253. package/dist/tool-surface/crib-sheet.js.map +1 -0
  254. package/dist/tool-surface/mcp-tool-source.d.ts +64 -0
  255. package/dist/tool-surface/mcp-tool-source.d.ts.map +1 -0
  256. package/dist/tool-surface/mcp-tool-source.js +189 -0
  257. package/dist/tool-surface/mcp-tool-source.js.map +1 -0
  258. package/dist/types/dsl/index.d.ts +4 -0
  259. package/dist/types/dsl/index.d.ts.map +1 -1
  260. package/dist/utils/event-catalog.d.ts +141 -10
  261. package/dist/utils/event-catalog.d.ts.map +1 -1
  262. package/dist/utils/event-catalog.js +23 -1
  263. package/dist/utils/event-catalog.js.map +1 -1
  264. package/dist/utils/perp-coin.d.ts.map +1 -1
  265. package/dist/utils/perp-coin.js +2 -1
  266. package/dist/utils/perp-coin.js.map +1 -1
  267. package/dist/utils/resolve-order-type.d.ts.map +1 -1
  268. package/dist/utils/resolve-order-type.js +5 -1
  269. package/dist/utils/resolve-order-type.js.map +1 -1
  270. package/dist/validate/codes.d.ts +2 -0
  271. package/dist/validate/codes.d.ts.map +1 -1
  272. package/dist/validate/codes.js +6 -0
  273. package/dist/validate/codes.js.map +1 -1
  274. package/dist/validate/index.d.ts +1 -1
  275. package/dist/validate/index.d.ts.map +1 -1
  276. package/dist/validate/index.js +1 -1
  277. package/dist/validate/index.js.map +1 -1
  278. package/dist/validate/recipe-checks.d.ts +7 -0
  279. package/dist/validate/recipe-checks.d.ts.map +1 -1
  280. package/dist/validate/recipe-checks.js +18 -0
  281. package/dist/validate/recipe-checks.js.map +1 -1
  282. package/dist/validate/recipe.d.ts.map +1 -1
  283. package/dist/validate/recipe.js +2 -1
  284. package/dist/validate/recipe.js.map +1 -1
  285. package/dist/widgets/tool.d.ts +9 -0
  286. package/dist/widgets/tool.d.ts.map +1 -1
  287. package/dist/widgets/tool.js +1 -0
  288. package/dist/widgets/tool.js.map +1 -1
  289. package/examples/strategies/README.md +2 -1
  290. package/examples/strategies/iguana/README.md +3 -3
  291. package/examples/strategies/raptor/README.md +3 -3
  292. package/openclaw.plugin.json +11 -1
  293. package/package.json +5 -2
@@ -8,6 +8,21 @@
8
8
  * - subagent_* lifecycle → `invoke_agent` (INTERNAL), parented to the model span
9
9
  * - agent_end → ends any still-open spans for the run
10
10
  *
11
+ * On OpenClaw 10.x (`modernHost`):
12
+ * - there is no `subagent_spawning` hook, so `subagent_spawned` opens the `invoke_agent` span;
13
+ * - the tool-search tools (`tool_search`, `tool_describe`, and the dispatcher `dispatch_action`,
14
+ * `tool_call` on 10.4) get `execute_tool` spans with `senpi.tool.bridge = true` and count on
15
+ * `tool.bridge.calls`, not `tool.calls`. The inner tool a dispatcher runs (id
16
+ * `tool_call:<sanitized outer id>:…`) nests under the dispatcher's span. Its own
17
+ * `after_tool_call` normally closes it (10.10 `embedded-agent-subscribe.handlers.tools.completion.ts:603-626`);
18
+ * any inner span still open when the dispatcher's `after_tool_call` fires is closed there with
19
+ * the outer outcome, and a late inner `after_tool_call` for it is then ignored, so each inner call
20
+ * is counted and evented once under its own name.
21
+ *
22
+ * On both hosts each `chat` span carries `senpi.llm.calls` (the run's `model_call_ended` count),
23
+ * and the drift alarm skips any unoffered name the host ran in the run (a direct call to a deferred
24
+ * tool is legal in 10.x tool-search directory mode).
25
+ *
11
26
  * Open and finalize are separate callbacks, so we can't use `withSpan`; we `startSpan`
12
27
  * and hold the handle in a session-keyed registry until the finalize hook, recording
13
28
  * errors directly on the held span (it's never the active span, so `logger.error` won't).
@@ -21,7 +36,7 @@ import { SpanKind, context as otelContext, propagation, trace, } from "@opentele
21
36
  import { getTracer } from "../../telemetry/tracing.js";
22
37
  import { HEADER_ADDRESS, HEADER_CORRELATION_ID, HEADER_RECIPE_NAME, HEADER_RUNTIME_ID, HEADER_RUNTIME_VERSION, } from "./constants.js";
23
38
  import { GEN_AI, errorTypeOf, firstNumber, normalizeGenAiMessages, recordSpanError, setDefinedAttributes, setGenAiContentAttribute, toFinishReasons, } from "./gen-ai.js";
24
- import { elapsedSeconds, recordToolCall, recordInvokeAgentDuration, recordOperationDuration, recordTokenUsage, } from "../../telemetry/metrics.js";
39
+ import { elapsedSeconds, recordToolBridgeCall, recordToolCall, recordInvokeAgentDuration, recordOperationDuration, recordTokenUsage, } from "../../telemetry/metrics.js";
25
40
  import { emitToolCalled, offeredToolNames, requestedToolNames, } from "../../telemetry/tool-plane.js";
26
41
  import { createLogger } from "../../utils/logger.js";
27
42
  const logger = createLogger("[senpi-core][span-hooks]");
@@ -30,6 +45,23 @@ const logger = createLogger("[senpi-core][span-hooks]");
30
45
  * `skill_router.decision` event shares (`src/utils/event-catalog.ts`). Distinct from `senpi.run.id`.
31
46
  */
32
47
  const OPENCLAW_RUN_ID_ATTR = "senpi.openclaw.run_id";
48
+ /** The tool-search bridge tools and their `senpi.tool.bridge_kind` (10.10 `src/agents/tool-search-types.ts:10-12`; `tool_call` is the 10.4 dispatcher). */
49
+ const BRIDGE_TOOL_KINDS = {
50
+ tool_search: "search",
51
+ tool_describe: "describe",
52
+ dispatch_action: "call",
53
+ tool_call: "call",
54
+ };
55
+ /**
56
+ * Port of OpenClaw's inner-call id sanitizer
57
+ * (`openclaw-worktrees/v2026.10.10-beta.3/src/agents/tool-search-runtime.ts:211-214`): the inner
58
+ * call a dispatcher runs gets the id `tool_call:${sanitizeToolCallIdPart(outerToolCallId)}:…`
59
+ * (the literal `tool_call:` prefix is kept on 10.10, where only the dispatcher was renamed).
60
+ */
61
+ export function sanitizeToolCallIdPart(value) {
62
+ const safe = value.trim().replace(/[^A-Za-z0-9_.:-]+/g, "_");
63
+ return safe.slice(0, 120) || "call";
64
+ }
33
65
  /**
34
66
  * Grace window the `agent_end` sweep holds a finished model span before ending it, so a
35
67
  * trailing `llm_output` (observed ~1 ms later in the codex/harness flow, but timed generously)
@@ -37,9 +69,20 @@ const OPENCLAW_RUN_ID_ATTR = "senpi.openclaw.run_id";
37
69
  * process open; an orphan run with no `llm_output` lets the timer end the span.
38
70
  */
39
71
  export const MODEL_SPAN_END_GRACE_MS = 2000;
72
+ /**
73
+ * Registers the span hooks.
74
+ *
75
+ * @returns How many hooks were registered (10 on 5.x; 9 on 10.x, which has no `subagent_spawning`).
76
+ */
40
77
  export function registerSpanHooks(options) {
41
78
  const { api } = options;
79
+ const modernHost = options.modernHost === true;
42
80
  const tracer = getTracer();
81
+ let hookCount = 0;
82
+ const on = (hookName, handler, opts) => {
83
+ api.on(hookName, handler, opts);
84
+ hookCount += 1;
85
+ };
43
86
  // Cap on drift events per turn so a flood of invented tool calls can't balloon the log.
44
87
  const MAX_DRIFT_EVENTS_PER_TURN = 8;
45
88
  // Held model spans, keyed by (sessionKey, runId). The tool/subagent spans parent
@@ -54,13 +97,23 @@ export function registerSpanHooks(options) {
54
97
  // (agentId → sessionKey/runId) from llm_input, plus the last-seen pair, so tool
55
98
  // hooks can resolve their host session.
56
99
  const sessionByAgentId = new Map();
100
+ // Latest run per session, for a 10.x subagent_spawned whose ctx names the requester session but
101
+ // not its run (ctx.runId is the child run there).
102
+ const latestRunBySession = new Map();
57
103
  let lastActive = null;
58
104
  const rememberSession = (agentCtx, sessionKey, runId) => {
59
105
  const agentId = nonEmptyString(asRecord(agentCtx)?.agentId);
60
106
  if (agentId)
61
107
  sessionByAgentId.set(agentId, { sessionKey, runId });
108
+ latestRunBySession.set(sessionKey, runId);
62
109
  lastActive = { sessionKey, runId };
63
110
  };
111
+ /** Stamps the run's model-call count on a `chat` span about to end. */
112
+ const stampLlmCalls = (held) => {
113
+ if (held.llmCalls && held.llmCalls > 0) {
114
+ setDefinedAttributes(held.span, { "senpi.llm.calls": held.llmCalls });
115
+ }
116
+ };
64
117
  const resolveToolSession = (ctx) => {
65
118
  const direct = {
66
119
  sessionKey: readSessionKey(ctx, undefined),
@@ -86,7 +139,7 @@ export function registerSpanHooks(options) {
86
139
  return model ? trace.setSpan(otelContext.active(), model.span) : otelContext.active();
87
140
  };
88
141
  // ─── LLM (`chat` span) ───────────────────────────────────────────────────
89
- api.on("llm_input", (event, agentCtx) => {
142
+ on("llm_input", (event, agentCtx) => {
90
143
  try {
91
144
  const identity = readIdentity(agentCtx);
92
145
  const sessionKey = readSessionKey(agentCtx, event);
@@ -100,8 +153,13 @@ export function registerSpanHooks(options) {
100
153
  // A duplicate llm_input for the same (session, run) means a new turn opened
101
154
  // without the previous one closing — end the prior span before replacing it.
102
155
  const prior = modelSpans.get(key);
103
- if (prior)
156
+ if (prior) {
157
+ // Its agent_end grace timer would end the replaced span a second time.
158
+ if (prior.endTimer)
159
+ clearTimeout(prior.endTimer);
160
+ stampLlmCalls(prior);
104
161
  endSpan(prior.span);
162
+ }
105
163
  const ev = asRecord(event) ?? {};
106
164
  const requestModel = nonEmptyString(ev.model);
107
165
  const provider = nonEmptyString(ev.provider);
@@ -148,7 +206,7 @@ export function registerSpanHooks(options) {
148
206
  logger.warn("llm_input handler failed", { err: errMsg(err) });
149
207
  }
150
208
  });
151
- api.on("llm_output", (event, agentCtx) => {
209
+ on("llm_output", (event, agentCtx) => {
152
210
  try {
153
211
  const sessionKey = readSessionKey(agentCtx, event);
154
212
  const runId = readRunId(agentCtx, event);
@@ -167,6 +225,7 @@ export function registerSpanHooks(options) {
167
225
  // Cancel any deferred teardown the agent_end sweep scheduled — we finalize the span here.
168
226
  if (held.endTimer)
169
227
  clearTimeout(held.endTimer);
228
+ stampLlmCalls(held);
170
229
  const ev = asRecord(event) ?? {};
171
230
  const usage = asRecord(ev.usage);
172
231
  // Token field naming varies by provider/wrapper; try every known spelling.
@@ -201,9 +260,14 @@ export function registerSpanHooks(options) {
201
260
  // deduped + capped so a flood of invented calls can't balloon the log.
202
261
  if (held.offeredTools) {
203
262
  const offered = held.offeredTools;
263
+ // A name the host ran in this run is not invented: in 10.x tool-search directory mode a
264
+ // direct call to any deferred tool (a `senpi__` MCP tool or a deferred OpenClaw tool) is
265
+ // legal though not in the offered set (`tool-search-directory.ts:194-197` on 10.10). An
266
+ // unoffered name that never ran is drift on every host and in every mode.
267
+ const ran = held.ranTools;
204
268
  const seen = new Set();
205
269
  for (const name of requestedToolNames(outputMessages)) {
206
- if (offered.has(name) || seen.has(name))
270
+ if (offered.has(name) || seen.has(name) || ran?.has(name))
207
271
  continue;
208
272
  if (seen.size >= MAX_DRIFT_EVENTS_PER_TURN)
209
273
  break;
@@ -237,7 +301,7 @@ export function registerSpanHooks(options) {
237
301
  // The provider-call result: `outcome`, `failureKind`, timing. This is the ONLY
238
302
  // signal for a failed/aborted turn — `llm_output` never fires when the call
239
303
  // errors, so without this a wedged turn is indistinguishable from a healthy one.
240
- api.on("model_call_ended", (event, agentCtx) => {
304
+ on("model_call_ended", (event, agentCtx) => {
241
305
  try {
242
306
  const sessionKey = readSessionKey(agentCtx, event);
243
307
  const runId = readRunId(agentCtx, event);
@@ -246,6 +310,7 @@ export function registerSpanHooks(options) {
246
310
  const held = modelSpans.get(sessionLlmKey(sessionKey, runId));
247
311
  if (!held)
248
312
  return; // span already finalized, or not one we opened — best effort.
313
+ held.llmCalls = (held.llmCalls ?? 0) + 1;
249
314
  const ev = asRecord(event) ?? {};
250
315
  const outcome = nonEmptyString(ev.outcome);
251
316
  const failureKind = nonEmptyString(ev.failureKind);
@@ -270,7 +335,59 @@ export function registerSpanHooks(options) {
270
335
  }
271
336
  });
272
337
  // ─── Tool (`execute_tool` span) ────────────────────────────────────────────
273
- api.on("before_tool_call", (event, toolCtx) => {
338
+ // Inner calls a dispatcher's after_tool_call closed, so their own late after_tool_call is not
339
+ // counted again; the value says whether that close already evented a failure (the outer call
340
+ // errored). FIFO-bounded: a host that never sends that late hook must not grow it.
341
+ const closedByDispatcher = new Map();
342
+ const CLOSED_BY_DISPATCHER_MAX = 512;
343
+ const innerKey = (sessionKey, runId, toolCallId) => `${sessionKey} ${runId} ${toolCallId}`;
344
+ const markClosedByDispatcher = (key, failureEvented) => {
345
+ closedByDispatcher.set(key, failureEvented);
346
+ while (closedByDispatcher.size > CLOSED_BY_DISPATCHER_MAX) {
347
+ const oldest = closedByDispatcher.keys().next().value;
348
+ closedByDispatcher.delete(oldest);
349
+ }
350
+ };
351
+ /**
352
+ * A 10.x dispatcher's open `execute_tool` span in this run whose inner-call prefix
353
+ * `tool_call:${sanitize(outer id)}:` starts `innerToolCallId`, for parenting the inner span.
354
+ */
355
+ const findDispatcherSpanFor = (sessionKey, runId, innerToolCallId) => {
356
+ for (const held of toolSpans.values()) {
357
+ if (held.bridgeKind !== "call" || !held.toolCallId)
358
+ continue;
359
+ if (held.sessionKey !== sessionKey || held.runId !== runId)
360
+ continue;
361
+ if (innerToolCallId.startsWith(`tool_call:${sanitizeToolCallIdPart(held.toolCallId)}:`))
362
+ return held;
363
+ }
364
+ return undefined;
365
+ };
366
+ /** Records the outcome on a held tool span and ends it, after its args stamping settles. */
367
+ const finalizeToolSpan = (held, errored, error, result) => {
368
+ const span = held.span;
369
+ void (async () => {
370
+ try {
371
+ if (held.pending)
372
+ await held.pending;
373
+ if (errored) {
374
+ recordSpanError(span, error);
375
+ }
376
+ else {
377
+ await setGenAiContentAttribute(span, GEN_AI.TOOL_CALL_RESULT, result);
378
+ }
379
+ }
380
+ finally {
381
+ try {
382
+ span.end();
383
+ }
384
+ catch {
385
+ /* best effort */
386
+ }
387
+ }
388
+ })();
389
+ };
390
+ on("before_tool_call", (event, toolCtx) => {
274
391
  try {
275
392
  const identity = readIdentity(toolCtx);
276
393
  const resolved = resolveToolSession(toolCtx);
@@ -283,14 +400,27 @@ export function registerSpanHooks(options) {
283
400
  const toolCallId = nonEmptyString(ev?.toolCallId) ??
284
401
  nonEmptyString(asRecord(toolCtx)?.toolCallId) ??
285
402
  null;
403
+ const model = modelSpans.get(sessionLlmKey(resolved.sessionKey, resolved.runId));
404
+ if (model)
405
+ (model.ranTools ??= new Set()).add(toolName);
406
+ const bridgeKind = modernHost ? BRIDGE_TOOL_KINDS[toolName] : undefined;
407
+ // An inner call a 10.x dispatcher runs nests under the dispatcher's span.
408
+ const dispatcher = modernHost && !bridgeKind && toolCallId?.startsWith("tool_call:")
409
+ ? findDispatcherSpanFor(resolved.sessionKey, resolved.runId, toolCallId)
410
+ : undefined;
286
411
  const key = toolKey(resolved.sessionKey, resolved.runId, toolCallId, toolName, ++toolSeq);
287
- const span = tracer.startSpan(`execute_tool ${toolName}`, { kind: SpanKind.INTERNAL }, parentContextForModel(resolved.sessionKey, resolved.runId));
412
+ const span = tracer.startSpan(`execute_tool ${toolName}`, { kind: SpanKind.INTERNAL }, dispatcher
413
+ ? trace.setSpan(otelContext.active(), dispatcher.span)
414
+ : parentContextForModel(resolved.sessionKey, resolved.runId));
288
415
  const held = {
289
416
  span,
290
417
  sessionKey: resolved.sessionKey,
291
418
  runId: resolved.runId,
292
419
  startMs: Date.now(),
293
420
  toolName,
421
+ ...(toolCallId ? { toolCallId } : {}),
422
+ ...(bridgeKind ? { bridgeKind } : {}),
423
+ ...(bridgeKind === "call" ? { requestedId: nonEmptyString(asRecord(ev?.params)?.id) } : {}),
294
424
  };
295
425
  toolSpans.set(key, held);
296
426
  setDefinedAttributes(span, {
@@ -299,6 +429,7 @@ export function registerSpanHooks(options) {
299
429
  [OPENCLAW_RUN_ID_ATTR]: resolved.runId,
300
430
  [GEN_AI.TOOL_NAME]: toolName,
301
431
  [GEN_AI.TOOL_CALL_ID]: toolCallId ?? undefined,
432
+ "senpi.tool.bridge": bridgeKind ? true : undefined,
302
433
  // Best-effort skill attribution from a skill-file read (no first-class skill hook exists).
303
434
  "senpi.skill.name": skillNameFromToolArgs(ev?.params ?? ev?.input),
304
435
  ...identityAttributes(identity),
@@ -311,42 +442,117 @@ export function registerSpanHooks(options) {
311
442
  logger.warn("before_tool_call handler failed", { err: errMsg(err) });
312
443
  }
313
444
  });
314
- api.on("after_tool_call", (event, toolCtx) => {
445
+ on("after_tool_call", (event, toolCtx) => {
315
446
  try {
316
447
  const resolved = resolveToolSession(toolCtx);
317
- if (!resolved) {
318
- logger.debug("after_tool_call session unresolved; skipping");
319
- return;
320
- }
321
448
  const ev = asRecord(event);
322
449
  const toolName = nonEmptyString(ev?.toolName) ?? nonEmptyString(ev?.name) ?? "unknown";
323
450
  const error = ev?.error;
324
451
  const errored = error !== undefined && error !== null;
325
- // Tool-plane event (narrative): a failed tool call — the agent-blind hole, greppable instead
326
- // of span-only. A successful call already rides its span + (state-changing) its domain event,
327
- // so it is not re-evented here. Emitted before the span lookup so a failure is never lost when
328
- // the matching before_tool_call span wasn't registered.
329
- if (errored) {
452
+ const result = ev?.result;
453
+ const toolCallId = nonEmptyString(ev?.toolCallId) ??
454
+ nonEmptyString(asRecord(toolCtx)?.toolCallId) ??
455
+ null;
456
+ const bridgeKind = modernHost ? BRIDGE_TOOL_KINDS[toolName] : undefined;
457
+ const emitFailure = (name, bridge) => {
330
458
  emitToolCalled({
331
- name: toolName,
459
+ name,
332
460
  origin: "agent",
333
461
  outcome: "error",
334
462
  errorType: errorTypeOf(error),
335
463
  errorMessage: error instanceof Error ? error.message : String(error),
464
+ ...(bridge ? { bridge: true } : {}),
336
465
  });
466
+ };
467
+ const foundKey = resolved
468
+ ? findToolKey(toolSpans, resolved.sessionKey, resolved.runId, toolCallId, toolName)
469
+ : undefined;
470
+ const held = foundKey ? toolSpans.get(foundKey) : undefined;
471
+ if (foundKey)
472
+ toolSpans.delete(foundKey);
473
+ if (bridgeKind) {
474
+ // Inner calls of this dispatcher still open: their own after_tool_call normally closed them
475
+ // first (10.10/10.4), so these are the ones it missed. They take the outer outcome here,
476
+ // count once under their own names, and a late after_tool_call for them is ignored.
477
+ const inner = [];
478
+ if (bridgeKind === "call" && resolved && toolCallId) {
479
+ const prefix = `tool_call:${sanitizeToolCallIdPart(toolCallId)}:`;
480
+ for (const [k, h] of toolSpans) {
481
+ if (h.sessionKey !== resolved.sessionKey || h.runId !== resolved.runId)
482
+ continue;
483
+ if (!h.toolCallId?.startsWith(prefix))
484
+ continue;
485
+ toolSpans.delete(k);
486
+ markClosedByDispatcher(innerKey(h.sessionKey, h.runId, h.toolCallId), errored);
487
+ inner.push(h);
488
+ }
489
+ }
490
+ if (errored) {
491
+ if (inner.length > 0) {
492
+ for (const h of inner)
493
+ emitFailure(h.toolName ?? "unknown");
494
+ }
495
+ else if (bridgeKind === "call" && held?.innerErrored) {
496
+ // The inner call's own after_tool_call already reported this failure.
497
+ }
498
+ else if (bridgeKind === "call") {
499
+ // No inner call, or one that succeeded: the dispatcher's own failure (a cap, a result
500
+ // post-processing error) is still a failure the agent saw, so it is always evented.
501
+ const requested = held?.requestedId ?? nonEmptyString(asRecord(ev?.params)?.id);
502
+ emitFailure(requested ?? toolName, requested !== undefined);
503
+ }
504
+ else {
505
+ emitFailure(toolName);
506
+ }
507
+ }
508
+ for (const h of inner) {
509
+ recordToolCall({
510
+ toolName: h.toolName,
511
+ origin: "agent",
512
+ outcome: errored ? "error" : "ok",
513
+ errorType: errored ? errorTypeOf(error) : undefined,
514
+ durationSec: elapsedSeconds(h.startMs),
515
+ });
516
+ finalizeToolSpan(h, errored, error, result);
517
+ }
518
+ recordToolBridgeCall({ kind: bridgeKind, outcome: errored ? "error" : "ok" });
519
+ if (held)
520
+ finalizeToolSpan(held, errored, error, result);
521
+ return;
337
522
  }
338
- const toolCallId = nonEmptyString(ev?.toolCallId) ??
339
- nonEmptyString(asRecord(toolCtx)?.toolCallId) ??
340
- null;
341
- const foundKey = findToolKey(toolSpans, resolved.sessionKey, resolved.runId, toolCallId, toolName);
342
- if (!foundKey) {
523
+ // A late after_tool_call for an inner call its dispatcher already closed and counted. Its
524
+ // span and metric are done; a failure it reports is still evented unless the dispatcher's
525
+ // close already did (a dispatcher can return an inner error to the model as a result).
526
+ if (modernHost && resolved && toolCallId) {
527
+ const k = innerKey(resolved.sessionKey, resolved.runId, toolCallId);
528
+ const failureEvented = closedByDispatcher.get(k);
529
+ if (failureEvented !== undefined) {
530
+ closedByDispatcher.delete(k);
531
+ if (errored && !failureEvented)
532
+ emitFailure(toolName);
533
+ return;
534
+ }
535
+ }
536
+ // Tool-plane event (narrative): a failed tool call — the agent-blind hole, greppable instead
537
+ // of span-only. A successful call already rides its span + (state-changing) its domain event,
538
+ // so it is not re-evented here. Emitted before the span check so a failure is never lost when
539
+ // the matching before_tool_call span wasn't registered.
540
+ if (errored)
541
+ emitFailure(toolName);
542
+ // Tell the still-open dispatcher that its inner call's failure is already reported.
543
+ if (errored && modernHost && resolved && toolCallId?.startsWith("tool_call:")) {
544
+ const dispatcher = findDispatcherSpanFor(resolved.sessionKey, resolved.runId, toolCallId);
545
+ if (dispatcher)
546
+ dispatcher.innerErrored = true;
547
+ }
548
+ if (!resolved) {
549
+ logger.debug("after_tool_call session unresolved; skipping");
550
+ return;
551
+ }
552
+ if (!held) {
343
553
  logger.debug("after_tool_call: no open tool span", { toolName, toolCallId });
344
554
  return;
345
555
  }
346
- const held = toolSpans.get(foundKey);
347
- const span = held.span;
348
- toolSpans.delete(foundKey);
349
- const result = ev?.result;
350
556
  recordToolCall({
351
557
  toolName: held.toolName,
352
558
  origin: "agent",
@@ -354,83 +560,83 @@ export function registerSpanHooks(options) {
354
560
  errorType: errored ? errorTypeOf(error) : undefined,
355
561
  durationSec: elapsedSeconds(held.startMs),
356
562
  });
357
- void (async () => {
358
- try {
359
- if (held.pending)
360
- await held.pending;
361
- if (errored) {
362
- recordSpanError(span, error);
363
- }
364
- else {
365
- await setGenAiContentAttribute(span, GEN_AI.TOOL_CALL_RESULT, result);
366
- }
367
- }
368
- finally {
369
- try {
370
- span.end();
371
- }
372
- catch {
373
- /* best effort */
374
- }
375
- }
376
- })();
563
+ finalizeToolSpan(held, errored, error, result);
377
564
  }
378
565
  catch (err) {
379
566
  logger.warn("after_tool_call handler failed", { err: errMsg(err) });
380
567
  }
381
568
  });
382
- // ─── Subagent (`invoke_agent` span, four-hook lifecycle) ───────────────────
383
- api.on("subagent_spawning", (event, subagentCtx) => {
384
- try {
385
- const identity = readIdentity(subagentCtx);
386
- const ev = asRecord(event);
387
- const ctxObj = asRecord(subagentCtx);
388
- const hostSessionKey = nonEmptyString(ctxObj?.requesterSessionKey) ??
389
- nonEmptyString(ctxObj?.sessionKey) ??
390
- "";
391
- const hostRunId = nonEmptyString(ctxObj?.runId) ?? nonEmptyString(ctxObj?.requesterRunId) ?? "";
392
- const childSessionKey = nonEmptyString(ev?.childSessionKey) ?? nonEmptyString(ctxObj?.childSessionKey);
393
- if (!childSessionKey) {
394
- logger.warn("subagent_spawning missing childSessionKey; skipping span");
395
- return;
569
+ // ─── Subagent (`invoke_agent` span) ────────────────────────────────────────
570
+ // 5.x: subagent_spawning opens, subagent_spawned enriches. 10.x has no subagent_spawning, so
571
+ // subagent_spawned opens the span when none is held for the child.
572
+ /** Opens and holds the `invoke_agent` span for `childSessionKey`, hosted by the given run. */
573
+ const openSubagentSpan = (params) => {
574
+ const { childSessionKey, hostSessionKey, hostRunId, ev } = params;
575
+ const prior = subagentSpans.get(childSessionKey);
576
+ if (prior)
577
+ endSpan(prior.span);
578
+ const agentName = nonEmptyString(ev?.label) ?? nonEmptyString(ev?.agentId) ?? "unknown";
579
+ const span = tracer.startSpan(`invoke_agent ${agentName}`, { kind: SpanKind.INTERNAL }, hostSessionKey && hostRunId
580
+ ? parentContextForModel(hostSessionKey, hostRunId)
581
+ : otelContext.active());
582
+ // sessionKey/runId carry the HOST run so the agent_end sweep can reach this span.
583
+ const held = {
584
+ span,
585
+ sessionKey: hostSessionKey,
586
+ runId: hostRunId,
587
+ startMs: Date.now(),
588
+ agentName,
589
+ };
590
+ subagentSpans.set(childSessionKey, held);
591
+ setDefinedAttributes(span, {
592
+ [GEN_AI.OPERATION_NAME]: "invoke_agent",
593
+ [GEN_AI.CONVERSATION_ID]: nonEmptyString(hostSessionKey),
594
+ [GEN_AI.AGENT_NAME]: agentName,
595
+ "senpi.subagent.mode": nonEmptyString(ev?.mode),
596
+ "senpi.subagent.requester": nonEmptyString(ev?.requester),
597
+ "senpi.subagent.thread_requested": typeof ev?.threadRequested === "boolean" ? ev.threadRequested : undefined,
598
+ ...identityAttributes(params.identity),
599
+ });
600
+ return held;
601
+ };
602
+ if (!modernHost) {
603
+ on("subagent_spawning", (event, subagentCtx) => {
604
+ try {
605
+ const ev = asRecord(event);
606
+ const ctxObj = asRecord(subagentCtx);
607
+ const hostSessionKey = nonEmptyString(ctxObj?.requesterSessionKey) ??
608
+ nonEmptyString(ctxObj?.sessionKey) ??
609
+ "";
610
+ const hostRunId = nonEmptyString(ctxObj?.runId) ?? nonEmptyString(ctxObj?.requesterRunId) ?? "";
611
+ const childSessionKey = nonEmptyString(ev?.childSessionKey) ?? nonEmptyString(ctxObj?.childSessionKey);
612
+ if (!childSessionKey) {
613
+ logger.warn("subagent_spawning missing childSessionKey; skipping span");
614
+ return;
615
+ }
616
+ openSubagentSpan({ childSessionKey, hostSessionKey, hostRunId, ev, identity: readIdentity(subagentCtx) });
396
617
  }
397
- const prior = subagentSpans.get(childSessionKey);
398
- if (prior)
399
- endSpan(prior.span);
400
- const agentName = nonEmptyString(ev?.label) ?? nonEmptyString(ev?.agentId) ?? "unknown";
401
- const span = tracer.startSpan(`invoke_agent ${agentName}`, { kind: SpanKind.INTERNAL }, hostSessionKey && hostRunId
402
- ? parentContextForModel(hostSessionKey, hostRunId)
403
- : otelContext.active());
404
- // sessionKey/runId carry the HOST run so the agent_end sweep can reach this span.
405
- subagentSpans.set(childSessionKey, {
406
- span,
407
- sessionKey: hostSessionKey,
408
- runId: hostRunId,
409
- startMs: Date.now(),
410
- agentName,
411
- });
412
- setDefinedAttributes(span, {
413
- [GEN_AI.OPERATION_NAME]: "invoke_agent",
414
- [GEN_AI.CONVERSATION_ID]: nonEmptyString(hostSessionKey),
415
- [GEN_AI.AGENT_NAME]: agentName,
416
- "senpi.subagent.mode": nonEmptyString(ev?.mode),
417
- "senpi.subagent.requester": nonEmptyString(ev?.requester),
418
- "senpi.subagent.thread_requested": typeof ev?.threadRequested === "boolean" ? ev.threadRequested : undefined,
419
- ...identityAttributes(identity),
420
- });
421
- }
422
- catch (err) {
423
- logger.warn("subagent_spawning handler failed", { err: errMsg(err) });
424
- }
425
- });
426
- api.on("subagent_spawned", (event, subagentCtx) => {
618
+ catch (err) {
619
+ logger.warn("subagent_spawning handler failed", { err: errMsg(err) });
620
+ }
621
+ });
622
+ }
623
+ on("subagent_spawned", (event, subagentCtx) => {
427
624
  try {
428
625
  const ev = asRecord(event);
429
626
  const ctxObj = asRecord(subagentCtx);
430
627
  const childSessionKey = nonEmptyString(ev?.childSessionKey) ?? nonEmptyString(ctxObj?.childSessionKey);
431
628
  if (!childSessionKey)
432
629
  return;
433
- const held = subagentSpans.get(childSessionKey);
630
+ let held = subagentSpans.get(childSessionKey);
631
+ if (!held && modernHost) {
632
+ // 10.x ctx: `runId` is the CHILD run; the host is the requester session, whose run is
633
+ // `requesterRunId` when given, else that session's latest run seen at llm_input.
634
+ const hostSessionKey = nonEmptyString(ctxObj?.requesterSessionKey) ?? nonEmptyString(ctxObj?.sessionKey) ?? "";
635
+ const hostRunId = nonEmptyString(ctxObj?.requesterRunId) ??
636
+ (hostSessionKey ? latestRunBySession.get(hostSessionKey) : undefined) ??
637
+ "";
638
+ held = openSubagentSpan({ childSessionKey, hostSessionKey, hostRunId, ev, identity: readIdentity(subagentCtx) });
639
+ }
434
640
  if (!held)
435
641
  return;
436
642
  const span = held.span;
@@ -444,7 +650,7 @@ export function registerSpanHooks(options) {
444
650
  logger.warn("subagent_spawned handler failed", { err: errMsg(err) });
445
651
  }
446
652
  });
447
- api.on("subagent_delivery_target", (event, subagentCtx) => {
653
+ on("subagent_delivery_target", (event, subagentCtx) => {
448
654
  try {
449
655
  const ev = asRecord(event);
450
656
  const ctxObj = asRecord(subagentCtx);
@@ -468,7 +674,7 @@ export function registerSpanHooks(options) {
468
674
  logger.warn("subagent_delivery_target handler failed", { err: errMsg(err) });
469
675
  }
470
676
  });
471
- api.on("subagent_ended", (event, subagentCtx) => {
677
+ on("subagent_ended", (event, subagentCtx) => {
472
678
  try {
473
679
  const ev = asRecord(event);
474
680
  const ctxObj = asRecord(subagentCtx);
@@ -509,7 +715,7 @@ export function registerSpanHooks(options) {
509
715
  }
510
716
  });
511
717
  // ─── Agent end (orphan sweep) ──────────────────────────────────────────────
512
- api.on("agent_end", (event, agentCtx) => {
718
+ on("agent_end", (event, agentCtx) => {
513
719
  try {
514
720
  const sessionKey = readSessionKey(agentCtx, event);
515
721
  const runId = readRunId(agentCtx, event);
@@ -542,6 +748,7 @@ export function registerSpanHooks(options) {
542
748
  model.endTimer = setTimeout(() => {
543
749
  if (modelSpans.get(llmKey) === model)
544
750
  modelSpans.delete(llmKey);
751
+ stampLlmCalls(model);
545
752
  endSpan(model.span);
546
753
  }, MODEL_SPAN_END_GRACE_MS);
547
754
  model.endTimer.unref?.();
@@ -568,11 +775,14 @@ export function registerSpanHooks(options) {
568
775
  if (lastActive && lastActive.sessionKey === sessionKey && lastActive.runId === runId) {
569
776
  lastActive = null;
570
777
  }
778
+ if (latestRunBySession.get(sessionKey) === runId)
779
+ latestRunBySession.delete(sessionKey);
571
780
  }
572
781
  catch (err) {
573
782
  logger.warn("agent_end handler failed", { err: errMsg(err) });
574
783
  }
575
784
  });
785
+ return hookCount;
576
786
  }
577
787
  // ─── Span key helpers ───────────────────────────────────────────────────────
578
788
  const SEP = " ";