@namzu/sdk 32.0.0 → 33.1.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 (305) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/dist/authorization/gate.d.ts +1 -1
  3. package/dist/authorization/gate.js +1 -1
  4. package/dist/bridge/a2a/mapper.d.ts.map +1 -1
  5. package/dist/bridge/a2a/mapper.js +13 -0
  6. package/dist/bridge/a2a/mapper.js.map +1 -1
  7. package/dist/bridge/sse/mapper.d.ts.map +1 -1
  8. package/dist/bridge/sse/mapper.js +3 -0
  9. package/dist/bridge/sse/mapper.js.map +1 -1
  10. package/dist/connector/index.d.ts +1 -1
  11. package/dist/connector/index.d.ts.map +1 -1
  12. package/dist/connector/index.js +1 -1
  13. package/dist/connector/index.js.map +1 -1
  14. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  15. package/dist/connector/mcp/adapter.js +71 -16
  16. package/dist/connector/mcp/adapter.js.map +1 -1
  17. package/dist/connector/mcp/client.d.ts.map +1 -1
  18. package/dist/connector/mcp/client.js +3 -2
  19. package/dist/connector/mcp/client.js.map +1 -1
  20. package/dist/connector/mcp/http-redirect.d.ts +17 -0
  21. package/dist/connector/mcp/http-redirect.d.ts.map +1 -0
  22. package/dist/connector/mcp/http-redirect.js +31 -0
  23. package/dist/connector/mcp/http-redirect.js.map +1 -0
  24. package/dist/connector/mcp/http-sse.d.ts.map +1 -1
  25. package/dist/connector/mcp/http-sse.js +5 -0
  26. package/dist/connector/mcp/http-sse.js.map +1 -1
  27. package/dist/connector/mcp/image-admission.d.ts +16 -0
  28. package/dist/connector/mcp/image-admission.d.ts.map +1 -0
  29. package/dist/connector/mcp/image-admission.js +288 -0
  30. package/dist/connector/mcp/image-admission.js.map +1 -0
  31. package/dist/connector/mcp/stdio.d.ts +13 -0
  32. package/dist/connector/mcp/stdio.d.ts.map +1 -1
  33. package/dist/connector/mcp/stdio.js +66 -23
  34. package/dist/connector/mcp/stdio.js.map +1 -1
  35. package/dist/connector/mcp/streamable-http.d.ts.map +1 -1
  36. package/dist/connector/mcp/streamable-http.js +3 -0
  37. package/dist/connector/mcp/streamable-http.js.map +1 -1
  38. package/dist/constants/tools/index.js +1 -1
  39. package/dist/constants/tools/index.js.map +1 -1
  40. package/dist/execution/base.d.ts +13 -0
  41. package/dist/execution/base.d.ts.map +1 -1
  42. package/dist/execution/base.js +103 -8
  43. package/dist/execution/base.js.map +1 -1
  44. package/dist/execution/errors.d.ts +12 -0
  45. package/dist/execution/errors.d.ts.map +1 -0
  46. package/dist/execution/errors.js +21 -0
  47. package/dist/execution/errors.js.map +1 -0
  48. package/dist/execution/factory.d.ts.map +1 -1
  49. package/dist/execution/factory.js +1 -0
  50. package/dist/execution/factory.js.map +1 -1
  51. package/dist/execution/hybrid.d.ts +5 -0
  52. package/dist/execution/hybrid.d.ts.map +1 -1
  53. package/dist/execution/hybrid.js +36 -16
  54. package/dist/execution/hybrid.js.map +1 -1
  55. package/dist/execution/index.d.ts +1 -0
  56. package/dist/execution/index.d.ts.map +1 -1
  57. package/dist/execution/index.js +1 -0
  58. package/dist/execution/index.js.map +1 -1
  59. package/dist/execution/local.d.ts +13 -0
  60. package/dist/execution/local.d.ts.map +1 -1
  61. package/dist/execution/local.js +145 -19
  62. package/dist/execution/local.js.map +1 -1
  63. package/dist/execution/output.d.ts +27 -0
  64. package/dist/execution/output.d.ts.map +1 -0
  65. package/dist/execution/output.js +105 -0
  66. package/dist/execution/output.js.map +1 -0
  67. package/dist/execution/remote.d.ts +26 -2
  68. package/dist/execution/remote.d.ts.map +1 -1
  69. package/dist/execution/remote.js +86 -15
  70. package/dist/execution/remote.js.map +1 -1
  71. package/dist/plugin/lifecycle.d.ts.map +1 -1
  72. package/dist/plugin/lifecycle.js +9 -3
  73. package/dist/plugin/lifecycle.js.map +1 -1
  74. package/dist/process/kill-tree.d.ts +11 -13
  75. package/dist/process/kill-tree.d.ts.map +1 -1
  76. package/dist/process/kill-tree.js +11 -13
  77. package/dist/process/kill-tree.js.map +1 -1
  78. package/dist/provider/capabilities.d.ts +2 -0
  79. package/dist/provider/capabilities.d.ts.map +1 -1
  80. package/dist/provider/capabilities.js +6 -0
  81. package/dist/provider/capabilities.js.map +1 -1
  82. package/dist/provider/fallback.d.ts.map +1 -1
  83. package/dist/provider/fallback.js +31 -0
  84. package/dist/provider/fallback.js.map +1 -1
  85. package/dist/provider/idle-timeout.d.ts.map +1 -1
  86. package/dist/provider/idle-timeout.js +5 -0
  87. package/dist/provider/idle-timeout.js.map +1 -1
  88. package/dist/provider/retry.d.ts.map +1 -1
  89. package/dist/provider/retry.js +5 -0
  90. package/dist/provider/retry.js.map +1 -1
  91. package/dist/public-runtime.d.ts +2 -2
  92. package/dist/public-runtime.d.ts.map +1 -1
  93. package/dist/public-runtime.js +2 -2
  94. package/dist/public-runtime.js.map +1 -1
  95. package/dist/registry/tool/execute.d.ts +7 -1
  96. package/dist/registry/tool/execute.d.ts.map +1 -1
  97. package/dist/registry/tool/execute.js +201 -54
  98. package/dist/registry/tool/execute.js.map +1 -1
  99. package/dist/run/command-gate.d.ts.map +1 -1
  100. package/dist/run/command-gate.js +11 -0
  101. package/dist/run/command-gate.js.map +1 -1
  102. package/dist/run/workspace-fingerprint.d.ts +4 -4
  103. package/dist/run/workspace-fingerprint.d.ts.map +1 -1
  104. package/dist/run/workspace-fingerprint.js +6 -4
  105. package/dist/run/workspace-fingerprint.js.map +1 -1
  106. package/dist/runtime/bidi/index.d.ts +1 -1
  107. package/dist/runtime/bidi/index.d.ts.map +1 -1
  108. package/dist/runtime/bidi/index.js +1 -1
  109. package/dist/runtime/bidi/index.js.map +1 -1
  110. package/dist/runtime/bidi/session.d.ts +19 -0
  111. package/dist/runtime/bidi/session.d.ts.map +1 -1
  112. package/dist/runtime/bidi/session.js +150 -26
  113. package/dist/runtime/bidi/session.js.map +1 -1
  114. package/dist/runtime/query/executor.d.ts +43 -1
  115. package/dist/runtime/query/executor.d.ts.map +1 -1
  116. package/dist/runtime/query/executor.js +626 -161
  117. package/dist/runtime/query/executor.js.map +1 -1
  118. package/dist/runtime/query/index.d.ts +3 -1
  119. package/dist/runtime/query/index.d.ts.map +1 -1
  120. package/dist/runtime/query/index.js +48 -4
  121. package/dist/runtime/query/index.js.map +1 -1
  122. package/dist/runtime/query/iteration/index.d.ts +10 -0
  123. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  124. package/dist/runtime/query/iteration/index.js +67 -0
  125. package/dist/runtime/query/iteration/index.js.map +1 -1
  126. package/dist/runtime/query/iteration/phases/context.d.ts +5 -0
  127. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  128. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  129. package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
  130. package/dist/runtime/query/iteration/phases/tool-review.js +84 -22
  131. package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
  132. package/dist/runtime/query/request-rich-content.d.ts.map +1 -1
  133. package/dist/runtime/query/request-rich-content.js +33 -13
  134. package/dist/runtime/query/request-rich-content.js.map +1 -1
  135. package/dist/runtime/query/result.d.ts.map +1 -1
  136. package/dist/runtime/query/result.js +25 -22
  137. package/dist/runtime/query/result.js.map +1 -1
  138. package/dist/runtime/query/resume-pending.d.ts +5 -1
  139. package/dist/runtime/query/resume-pending.d.ts.map +1 -1
  140. package/dist/runtime/query/resume-pending.js +66 -1
  141. package/dist/runtime/query/resume-pending.js.map +1 -1
  142. package/dist/runtime/query/tool-output-budget.d.ts.map +1 -1
  143. package/dist/runtime/query/tool-output-budget.js +45 -16
  144. package/dist/runtime/query/tool-output-budget.js.map +1 -1
  145. package/dist/runtime/query/tooling.d.ts +6 -0
  146. package/dist/runtime/query/tooling.d.ts.map +1 -1
  147. package/dist/runtime/query/tooling.js +4 -0
  148. package/dist/runtime/query/tooling.js.map +1 -1
  149. package/dist/sandbox/provider/local.d.ts.map +1 -1
  150. package/dist/sandbox/provider/local.js +193 -41
  151. package/dist/sandbox/provider/local.js.map +1 -1
  152. package/dist/scheduler/delegating.d.ts +2 -1
  153. package/dist/scheduler/delegating.d.ts.map +1 -1
  154. package/dist/scheduler/delegating.js +6 -5
  155. package/dist/scheduler/delegating.js.map +1 -1
  156. package/dist/scheduler/local.d.ts +2 -1
  157. package/dist/scheduler/local.d.ts.map +1 -1
  158. package/dist/scheduler/local.js +2 -2
  159. package/dist/scheduler/local.js.map +1 -1
  160. package/dist/session/workspace/driver.d.ts +12 -4
  161. package/dist/session/workspace/driver.d.ts.map +1 -1
  162. package/dist/session/workspace/git-worktree.d.ts +17 -3
  163. package/dist/session/workspace/git-worktree.d.ts.map +1 -1
  164. package/dist/session/workspace/git-worktree.js +153 -34
  165. package/dist/session/workspace/git-worktree.js.map +1 -1
  166. package/dist/skills/registry.d.ts +15 -0
  167. package/dist/skills/registry.d.ts.map +1 -1
  168. package/dist/skills/registry.js +33 -0
  169. package/dist/skills/registry.js.map +1 -1
  170. package/dist/tools/builtins/computer-use.d.ts.map +1 -1
  171. package/dist/tools/builtins/computer-use.js +76 -2
  172. package/dist/tools/builtins/computer-use.js.map +1 -1
  173. package/dist/tools/builtins/run-code.d.ts.map +1 -1
  174. package/dist/tools/builtins/run-code.js +9 -11
  175. package/dist/tools/builtins/run-code.js.map +1 -1
  176. package/dist/tools/builtins/skill.d.ts +2 -1
  177. package/dist/tools/builtins/skill.d.ts.map +1 -1
  178. package/dist/tools/builtins/skill.js +250 -6
  179. package/dist/tools/builtins/skill.js.map +1 -1
  180. package/dist/tools/coordinator/agent.d.ts.map +1 -1
  181. package/dist/tools/coordinator/agent.js +100 -30
  182. package/dist/tools/coordinator/agent.js.map +1 -1
  183. package/dist/types/agent/manager.d.ts +1 -1
  184. package/dist/types/agent/manager.d.ts.map +1 -1
  185. package/dist/types/agent/scheduler.d.ts +10 -1
  186. package/dist/types/agent/scheduler.d.ts.map +1 -1
  187. package/dist/types/authorization/index.d.ts +12 -12
  188. package/dist/types/bidi/index.d.ts +19 -2
  189. package/dist/types/bidi/index.d.ts.map +1 -1
  190. package/dist/types/computer-use/index.d.ts +18 -0
  191. package/dist/types/computer-use/index.d.ts.map +1 -1
  192. package/dist/types/connector/execution-context.d.ts +3 -1
  193. package/dist/types/connector/execution-context.d.ts.map +1 -1
  194. package/dist/types/errors/catalog.d.ts.map +1 -1
  195. package/dist/types/errors/catalog.js +13 -6
  196. package/dist/types/errors/catalog.js.map +1 -1
  197. package/dist/types/errors/index.d.ts.map +1 -1
  198. package/dist/types/errors/index.js +21 -8
  199. package/dist/types/errors/index.js.map +1 -1
  200. package/dist/types/execution/index.d.ts +43 -1
  201. package/dist/types/execution/index.d.ts.map +1 -1
  202. package/dist/types/hitl/index.d.ts +11 -0
  203. package/dist/types/hitl/index.d.ts.map +1 -1
  204. package/dist/types/hitl/index.js.map +1 -1
  205. package/dist/types/message/index.d.ts +1 -1
  206. package/dist/types/message/index.d.ts.map +1 -1
  207. package/dist/types/message/index.js +2 -1
  208. package/dist/types/message/index.js.map +1 -1
  209. package/dist/types/plugin/index.d.ts +14 -1
  210. package/dist/types/plugin/index.d.ts.map +1 -1
  211. package/dist/types/plugin/index.js +1 -0
  212. package/dist/types/plugin/index.js.map +1 -1
  213. package/dist/types/provider/config.d.ts +9 -0
  214. package/dist/types/provider/config.d.ts.map +1 -1
  215. package/dist/types/provider/interface.d.ts +14 -0
  216. package/dist/types/provider/interface.d.ts.map +1 -1
  217. package/dist/types/run/events.d.ts +17 -0
  218. package/dist/types/run/events.d.ts.map +1 -1
  219. package/dist/types/run/events.js.map +1 -1
  220. package/dist/types/sandbox/index.d.ts +15 -10
  221. package/dist/types/sandbox/index.d.ts.map +1 -1
  222. package/dist/types/sandbox/index.js.map +1 -1
  223. package/dist/types/tool/index.d.ts +77 -6
  224. package/dist/types/tool/index.d.ts.map +1 -1
  225. package/dist/types/tool/index.js.map +1 -1
  226. package/dist/types/tool/presentation.d.ts +6 -2
  227. package/dist/types/tool/presentation.d.ts.map +1 -1
  228. package/dist/types/workspace/ref.d.ts +5 -0
  229. package/dist/types/workspace/ref.d.ts.map +1 -1
  230. package/dist/utils/abort.d.ts +8 -0
  231. package/dist/utils/abort.d.ts.map +1 -1
  232. package/dist/utils/abort.js +16 -0
  233. package/dist/utils/abort.js.map +1 -1
  234. package/package.json +4 -4
  235. package/src/authorization/gate.ts +1 -1
  236. package/src/bridge/a2a/mapper.ts +23 -4
  237. package/src/bridge/sse/mapper.ts +3 -0
  238. package/src/connector/index.ts +2 -0
  239. package/src/connector/mcp/adapter.ts +82 -21
  240. package/src/connector/mcp/client.ts +2 -1
  241. package/src/connector/mcp/http-redirect.ts +32 -0
  242. package/src/connector/mcp/http-sse.ts +5 -0
  243. package/src/connector/mcp/image-admission.ts +272 -0
  244. package/src/connector/mcp/stdio.ts +63 -21
  245. package/src/connector/mcp/streamable-http.ts +3 -0
  246. package/src/constants/tools/index.ts +1 -1
  247. package/src/execution/base.ts +124 -7
  248. package/src/execution/errors.ts +24 -0
  249. package/src/execution/factory.ts +1 -0
  250. package/src/execution/hybrid.ts +47 -16
  251. package/src/execution/index.ts +1 -0
  252. package/src/execution/local.ts +164 -19
  253. package/src/execution/output.ts +104 -0
  254. package/src/execution/remote.ts +122 -17
  255. package/src/plugin/lifecycle.ts +9 -3
  256. package/src/process/kill-tree.ts +11 -13
  257. package/src/provider/capabilities.ts +10 -0
  258. package/src/provider/fallback.ts +39 -0
  259. package/src/provider/idle-timeout.ts +8 -0
  260. package/src/provider/retry.ts +8 -0
  261. package/src/public-runtime.ts +7 -1
  262. package/src/registry/tool/execute.ts +230 -64
  263. package/src/run/command-gate.ts +12 -0
  264. package/src/run/workspace-fingerprint.ts +5 -4
  265. package/src/runtime/bidi/index.ts +1 -1
  266. package/src/runtime/bidi/session.ts +177 -28
  267. package/src/runtime/query/executor.ts +769 -163
  268. package/src/runtime/query/index.ts +58 -7
  269. package/src/runtime/query/iteration/index.ts +72 -0
  270. package/src/runtime/query/iteration/phases/context.ts +5 -0
  271. package/src/runtime/query/iteration/phases/tool-review.ts +87 -23
  272. package/src/runtime/query/request-rich-content.ts +39 -14
  273. package/src/runtime/query/result.ts +25 -23
  274. package/src/runtime/query/resume-pending.ts +83 -1
  275. package/src/runtime/query/tool-output-budget.ts +52 -17
  276. package/src/runtime/query/tooling.ts +10 -0
  277. package/src/sandbox/provider/local.ts +213 -45
  278. package/src/scheduler/delegating.ts +6 -5
  279. package/src/scheduler/local.ts +3 -2
  280. package/src/session/workspace/driver.ts +12 -4
  281. package/src/session/workspace/git-worktree.ts +173 -34
  282. package/src/skills/registry.ts +47 -0
  283. package/src/tools/builtins/computer-use.ts +86 -2
  284. package/src/tools/builtins/run-code.ts +9 -11
  285. package/src/tools/builtins/skill.ts +325 -7
  286. package/src/tools/coordinator/agent.ts +108 -33
  287. package/src/types/agent/manager.ts +1 -1
  288. package/src/types/agent/scheduler.ts +10 -1
  289. package/src/types/bidi/index.ts +19 -2
  290. package/src/types/computer-use/index.ts +19 -0
  291. package/src/types/connector/execution-context.ts +3 -0
  292. package/src/types/errors/catalog.ts +14 -6
  293. package/src/types/errors/index.ts +22 -8
  294. package/src/types/execution/index.ts +46 -1
  295. package/src/types/hitl/index.ts +11 -0
  296. package/src/types/message/index.ts +3 -2
  297. package/src/types/plugin/index.ts +15 -0
  298. package/src/types/provider/config.ts +9 -0
  299. package/src/types/provider/interface.ts +18 -0
  300. package/src/types/run/events.ts +17 -3
  301. package/src/types/sandbox/index.ts +15 -10
  302. package/src/types/tool/index.ts +85 -6
  303. package/src/types/tool/presentation.ts +10 -3
  304. package/src/types/workspace/ref.ts +5 -0
  305. package/src/utils/abort.ts +18 -0
@@ -1,6 +1,7 @@
1
1
  import { spawn } from 'node:child_process'
2
2
  import { existsSync } from 'node:fs'
3
3
  import { resolve } from 'node:path'
4
+ import { killTree } from '../process/kill-tree.js'
4
5
  import type { LocalExecutionContextConfig } from '../types/connector/index.js'
5
6
  import type {
6
7
  CommandExecutor,
@@ -9,8 +10,22 @@ import type {
9
10
  ExecutionCapability,
10
11
  ExecutionEnvironment,
11
12
  } from '../types/execution/index.js'
13
+ import { subscribeToAbort } from '../utils/abort.js'
12
14
  import type { Logger } from '../utils/logger.js'
13
15
  import { BaseExecutionContext } from './base.js'
16
+ import { BoundedTailCapture } from './output.js'
17
+
18
+ const LOCAL_COMMAND_DEFAULT_TIMEOUT_MS = 30_000
19
+ const LOCAL_COMMAND_KILL_GRACE_MS = 3_000
20
+ const LOCAL_COMMAND_DEFAULT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024
21
+ const LOCAL_COMMAND_HARD_MAX_OUTPUT_BYTES = 64 * 1024 * 1024
22
+
23
+ type LocalCommandCancellation = 'caller' | 'deadline' | 'teardown'
24
+
25
+ interface ActiveLocalCommand {
26
+ readonly closed: Promise<void>
27
+ cancel(origin: LocalCommandCancellation): void
28
+ }
14
29
 
15
30
  export interface LocalExecutionContextOptions {
16
31
  id: string
@@ -19,6 +34,8 @@ export interface LocalExecutionContextOptions {
19
34
  envVars?: Record<string, string>
20
35
  capabilities?: ExecutionCapability[]
21
36
  shell?: string
37
+ /** Retained bytes per stdout/stderr stream. Defaults to 4 MiB; maximum 64 MiB. */
38
+ maxOutputBytes?: number
22
39
  log?: Logger
23
40
  }
24
41
 
@@ -31,6 +48,14 @@ export class LocalExecutionContext extends BaseExecutionContext implements Comma
31
48
  private envVars: Record<string, string>
32
49
  private capabilities: ExecutionCapability[]
33
50
  private shell: string | undefined
51
+ private maxOutputBytes: number
52
+ /**
53
+ * Constructor-created contexts are intentionally executable before
54
+ * `initialize()`: `createCommandGate()` has always used that public path.
55
+ * Teardown closes admission synchronously; an explicit initialize reopens it.
56
+ */
57
+ private acceptsCommands = true
58
+ private readonly activeCommands = new Set<ActiveLocalCommand>()
34
59
 
35
60
  constructor(options: LocalExecutionContextOptions) {
36
61
  super(options.log)
@@ -40,6 +65,17 @@ export class LocalExecutionContext extends BaseExecutionContext implements Comma
40
65
  this.envVars = options.envVars ?? {}
41
66
  this.capabilities = options.capabilities ?? ['filesystem', 'process', 'shell']
42
67
  this.shell = options.shell
68
+ const maxOutputBytes = options.maxOutputBytes ?? LOCAL_COMMAND_DEFAULT_MAX_OUTPUT_BYTES
69
+ if (
70
+ !Number.isSafeInteger(maxOutputBytes) ||
71
+ maxOutputBytes <= 0 ||
72
+ maxOutputBytes > LOCAL_COMMAND_HARD_MAX_OUTPUT_BYTES
73
+ ) {
74
+ throw new RangeError(
75
+ `The value of "maxOutputBytes" is out of range. It must be a positive safe integer no greater than ${LOCAL_COMMAND_HARD_MAX_OUTPUT_BYTES}. Received ${maxOutputBytes}`,
76
+ )
77
+ }
78
+ this.maxOutputBytes = maxOutputBytes
43
79
  }
44
80
 
45
81
  protected async doInitialize(): Promise<void> {
@@ -49,7 +85,23 @@ export class LocalExecutionContext extends BaseExecutionContext implements Comma
49
85
  this.log.info('Local context initialized', { 'namzu.execution.cwd': this.cwd })
50
86
  }
51
87
 
52
- protected async doTeardown(): Promise<void> {}
88
+ protected override onInitializationStarted(): void {
89
+ this.acceptsCommands = false
90
+ }
91
+
92
+ protected override onInitializationCommitted(): void {
93
+ this.acceptsCommands = true
94
+ }
95
+
96
+ protected override onTeardownRequested(): void {
97
+ this.acceptsCommands = false
98
+ }
99
+
100
+ protected async doTeardown(): Promise<void> {
101
+ const admitted = [...this.activeCommands]
102
+ for (const operation of admitted) operation.cancel('teardown')
103
+ await Promise.all(admitted.map((operation) => operation.closed))
104
+ }
53
105
 
54
106
  getCwd(): string {
55
107
  return this.cwd
@@ -92,63 +144,155 @@ export class LocalExecutionContext extends BaseExecutionContext implements Comma
92
144
  args: string[] = [],
93
145
  options?: CommandOptions,
94
146
  ): Promise<CommandResult> {
147
+ if (!this.acceptsCommands) {
148
+ throw new Error(
149
+ `Local execution context "${this.id}" is tearing down or torn down. Call initialize() before executing another command.`,
150
+ )
151
+ }
95
152
  if (!this.hasCapability('process') && !this.hasCapability('shell')) {
96
153
  return {
97
154
  exitCode: 1,
98
155
  stdout: '',
99
156
  stderr: 'Command execution not available: context lacks process/shell capability',
157
+ stdoutTruncated: false,
158
+ stderrTruncated: false,
100
159
  durationMs: 0,
101
160
  }
102
161
  }
103
162
 
104
163
  const cwd = options?.cwd ? resolve(this.cwd, options.cwd) : this.cwd
105
164
  const env = { ...process.env, ...this.envVars, ...options?.env }
106
- const timeoutMs = options?.timeoutMs ?? 30_000
165
+ const timeoutMs = options?.timeoutMs ?? LOCAL_COMMAND_DEFAULT_TIMEOUT_MS
166
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 0) {
167
+ throw new RangeError(
168
+ `The value of "timeoutMs" is out of range. It must be an unsigned integer. Received ${timeoutMs}`,
169
+ )
170
+ }
107
171
  // node's spawn() only keeps argv separation when it execs the binary directly;
108
172
  // with shell enabled it re-joins command+args into one unescaped string and hands
109
173
  // it to sh/cmd.exe, so any caller-supplied arg becomes shell syntax rather than a
110
174
  // literal value. Since `command`/`args` here are whatever a tool call passed in,
111
175
  // shell interpretation must be an explicit opt-in, never the silent default.
112
176
  const shell = options?.shell ?? this.shell ?? false
177
+ const callerSignal = options?.signal
178
+ if (callerSignal?.aborted) {
179
+ return {
180
+ exitCode: null,
181
+ stdout: '',
182
+ stderr: '',
183
+ stdoutTruncated: false,
184
+ stderrTruncated: false,
185
+ durationMs: 0,
186
+ termination: { origin: 'caller', admitted: false },
187
+ }
188
+ }
113
189
 
114
- return new Promise<CommandResult>((resolvePromise) => {
190
+ let operation: ActiveLocalCommand
191
+ const result = new Promise<CommandResult>((resolvePromise) => {
115
192
  const start = performance.now()
116
193
  const proc = spawn(command, args, {
117
194
  cwd,
118
195
  env,
119
196
  shell,
120
- timeout: timeoutMs,
197
+ // POSIX process-group ownership is the strongest boundary available
198
+ // to this host execution context. Windows uses taskkill /T in
199
+ // `killTree`; neither path relies on Node's direct-child timeout.
200
+ detached: process.platform !== 'win32',
121
201
  })
122
202
 
123
- let stdout = ''
124
- let stderr = ''
203
+ const stdout = new BoundedTailCapture(this.maxOutputBytes)
204
+ const stderr = new BoundedTailCapture(this.maxOutputBytes)
205
+ let spawnError: Error | undefined
206
+ let cancellation: LocalCommandCancellation | undefined
207
+ let escalation: ReturnType<typeof setTimeout> | undefined
208
+ let deadline: ReturnType<typeof setTimeout> | undefined
209
+ let disposeCallerAbort: (() => void) | undefined
210
+ let settled = false
211
+
212
+ const terminate = (signal: NodeJS.Signals): void => {
213
+ killTree(proc, signal)
214
+ }
215
+ const cancel = (origin: LocalCommandCancellation): void => {
216
+ if (settled || cancellation !== undefined) return
217
+ cancellation = origin
218
+ terminate('SIGTERM')
219
+ escalation = setTimeout(() => {
220
+ escalation = undefined
221
+ terminate('SIGKILL')
222
+ }, LOCAL_COMMAND_KILL_GRACE_MS)
223
+ escalation.unref?.()
224
+ }
225
+
226
+ let resolveClosed: (() => void) | undefined
227
+ const closed = new Promise<void>((resolveClose) => {
228
+ resolveClosed = resolveClose
229
+ })
230
+ operation = { cancel, closed }
231
+ this.activeCommands.add(operation)
232
+
233
+ if (timeoutMs > 0) {
234
+ deadline = setTimeout(() => cancel('deadline'), timeoutMs)
235
+ deadline.unref?.()
236
+ }
125
237
 
126
238
  proc.stdout?.on('data', (data: Buffer) => {
127
- stdout += data.toString()
239
+ stdout.push(data)
128
240
  })
129
241
 
130
242
  proc.stderr?.on('data', (data: Buffer) => {
131
- stderr += data.toString()
243
+ stderr.push(data)
132
244
  })
133
245
 
134
- proc.on('close', (code) => {
135
- resolvePromise({
136
- exitCode: code ?? 1,
137
- stdout,
138
- stderr,
139
- durationMs: Math.round(performance.now() - start),
140
- })
246
+ proc.once('error', (err) => {
247
+ // `close` follows `error` and is the point at which inherited stdio
248
+ // has drained. Releasing ownership here would strand a descendant
249
+ // which still holds those descriptors.
250
+ spawnError = err
141
251
  })
142
252
 
143
- proc.on('error', (err) => {
253
+ proc.once('close', (code, signal) => {
254
+ if (settled) return
255
+ settled = true
256
+ disposeCallerAbort?.()
257
+ if (deadline !== undefined) clearTimeout(deadline)
258
+ if (escalation !== undefined) {
259
+ clearTimeout(escalation)
260
+ escalation = undefined
261
+ // Do not retain a numeric process-group id for a later timer after
262
+ // close: the host can reuse it. Force the cancellation boundary now.
263
+ terminate('SIGKILL')
264
+ }
265
+ this.activeCommands.delete(operation)
266
+ resolveClosed?.()
267
+ if (spawnError !== undefined) {
268
+ if (stderr.retainedBytes > 0) stderr.push(Buffer.from('\n'))
269
+ stderr.push(Buffer.from(spawnError.message))
270
+ }
144
271
  resolvePromise({
145
- exitCode: 1,
146
- stdout,
147
- stderr: err.message,
272
+ exitCode: spawnError === undefined ? code : 1,
273
+ stdout: stdout.text,
274
+ stderr: stderr.text,
275
+ stdoutTruncated: stdout.truncated,
276
+ stderrTruncated: stderr.truncated,
148
277
  durationMs: Math.round(performance.now() - start),
278
+ ...(cancellation === undefined
279
+ ? {}
280
+ : {
281
+ termination: {
282
+ origin: cancellation === 'deadline' ? ('timeout' as const) : cancellation,
283
+ admitted: true as const,
284
+ ...(signal === null ? {} : { signal }),
285
+ },
286
+ }),
149
287
  })
150
288
  })
289
+
290
+ if (callerSignal !== undefined) {
291
+ disposeCallerAbort = subscribeToAbort(callerSignal, () => cancel('caller'))
292
+ if (callerSignal.aborted) cancel('caller')
293
+ }
151
294
  })
295
+ return result
152
296
  }
153
297
 
154
298
  toConfig(): LocalExecutionContextConfig {
@@ -160,6 +304,7 @@ export class LocalExecutionContext extends BaseExecutionContext implements Comma
160
304
  envVars: this.envVars,
161
305
  capabilities: this.capabilities,
162
306
  shell: this.shell,
307
+ maxOutputBytes: this.maxOutputBytes,
163
308
  }
164
309
  }
165
310
  }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Retain the newest bytes of one command-output stream under a hard bound.
3
+ *
4
+ * Bytes stay undecoded until the command closes, so a UTF-8 code point split
5
+ * across ordinary stream chunks is not corrupted. The backing store grows
6
+ * geometrically up to the configured cap and becomes a ring only when output
7
+ * crosses it; no incoming chunk is retained by reference.
8
+ */
9
+ export class BoundedTailCapture {
10
+ private storage = Buffer.alloc(0)
11
+ private start = 0
12
+ private length = 0
13
+ private lostBytes = false
14
+
15
+ constructor(private readonly capBytes: number) {
16
+ if (!Number.isSafeInteger(capBytes) || capBytes <= 0) {
17
+ throw new RangeError('BoundedTailCapture capBytes must be a positive safe integer')
18
+ }
19
+ }
20
+
21
+ push(chunk: Buffer): void {
22
+ if (chunk.length === 0) return
23
+
24
+ if (chunk.length >= this.capBytes) {
25
+ if (chunk.length > this.capBytes || this.length > 0) this.lostBytes = true
26
+ this.ensureCapacity(this.capBytes)
27
+ chunk.copy(this.storage, 0, chunk.length - this.capBytes)
28
+ this.start = 0
29
+ this.length = this.capBytes
30
+ return
31
+ }
32
+
33
+ const combinedLength = this.length + chunk.length
34
+ if (combinedLength <= this.capBytes) {
35
+ this.ensureCapacity(combinedLength)
36
+ chunk.copy(this.storage, this.length)
37
+ this.length = combinedLength
38
+ return
39
+ }
40
+
41
+ this.lostBytes = true
42
+ this.ensureCapacity(this.capBytes)
43
+ const end = (this.start + this.length) % this.capBytes
44
+ const first = Math.min(chunk.length, this.capBytes - end)
45
+ chunk.copy(this.storage, end, 0, first)
46
+ if (first < chunk.length) chunk.copy(this.storage, 0, first)
47
+ this.start = (this.start + combinedLength - this.capBytes) % this.capBytes
48
+ this.length = this.capBytes
49
+ }
50
+
51
+ /** Ordered copy of the retained raw bytes. */
52
+ get bytes(): Buffer {
53
+ const ordered = Buffer.allocUnsafe(this.length)
54
+ if (this.length === 0) return ordered
55
+ const first = Math.min(this.length, this.storage.length - this.start)
56
+ this.storage.copy(ordered, 0, this.start, this.start + first)
57
+ if (first < this.length) this.storage.copy(ordered, first, 0, this.length - first)
58
+ return ordered
59
+ }
60
+
61
+ get text(): string {
62
+ const ordered = this.bytes
63
+ let offset = 0
64
+ // Tail eviction can leave continuation bytes from a valid UTF-8 code
65
+ // point at the new left edge. They are an artifact of the cut, not an
66
+ // authored replacement character, so discard only that partial prefix.
67
+ if (this.lostBytes) {
68
+ while (offset < ordered.length) {
69
+ const firstByte = ordered[offset]
70
+ if (firstByte === undefined || (firstByte & 0xc0) !== 0x80) break
71
+ offset++
72
+ }
73
+ }
74
+ return ordered.toString('utf8', offset)
75
+ }
76
+
77
+ get truncated(): boolean {
78
+ return this.lostBytes
79
+ }
80
+
81
+ /** Diagnostic invariant used by the internal killing observers. */
82
+ get retainedBytes(): number {
83
+ return this.length
84
+ }
85
+
86
+ /** Diagnostic invariant used by the internal killing observers. */
87
+ get allocatedBytes(): number {
88
+ return this.storage.length
89
+ }
90
+
91
+ private ensureCapacity(required: number): void {
92
+ if (this.storage.length >= required) return
93
+ let capacity = Math.max(1, this.storage.length)
94
+ while (capacity < required) capacity = Math.min(this.capBytes, capacity * 2)
95
+ const grown = Buffer.allocUnsafe(capacity)
96
+ if (this.length > 0) {
97
+ const first = Math.min(this.length, this.storage.length - this.start)
98
+ this.storage.copy(grown, 0, this.start, this.start + first)
99
+ if (first < this.length) this.storage.copy(grown, first, 0, this.length - first)
100
+ }
101
+ this.storage = grown
102
+ this.start = 0
103
+ }
104
+ }
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ CommandExecutor,
2
3
  CommandOptions,
3
4
  CommandResult,
4
5
  ExecutionCapability,
@@ -9,29 +10,41 @@ import type {
9
10
  } from '../types/connector/index.js'
10
11
  import type { Logger } from '../utils/logger.js'
11
12
  import { BaseExecutionContext } from './base.js'
13
+ import { CommandCancellationUnsupportedError, RemoteExecutionBusyError } from './errors.js'
12
14
 
13
15
  export interface RemoteExecutionContextOptions {
14
16
  id: string
15
17
  target: RemoteTarget
16
18
  capabilities?: ExecutionCapability[]
19
+ /**
20
+ * Executes a command while preserving its argument boundaries at Namzu's
21
+ * remote seam. A downstream executor may still interpret `shell` according
22
+ * to its own target.
23
+ */
24
+ commandExecutor?: CommandExecutor
25
+ /** @deprecated Use `commandExecutor`. */
17
26
  commandHandler?: RemoteCommandHandler
18
27
  log?: Logger
19
28
  }
20
29
 
21
- export class RemoteExecutionContext extends BaseExecutionContext {
30
+ export class RemoteExecutionContext extends BaseExecutionContext implements CommandExecutor {
22
31
  readonly id: string
23
32
  readonly environment: ExecutionEnvironment = 'remote'
24
33
 
25
34
  private target: RemoteTarget
26
35
  private connected = false
27
36
  private capabilities: ExecutionCapability[]
37
+ private commandExecutor: CommandExecutor | undefined
28
38
  private commandHandler: RemoteCommandHandler | undefined
39
+ private lifecycleAdmissionsOpen = true
40
+ private readonly activeCommands = new Set<symbol>()
29
41
 
30
42
  constructor(options: RemoteExecutionContextOptions) {
31
43
  super(options.log)
32
44
  this.id = options.id
33
45
  this.target = options.target
34
46
  this.capabilities = options.capabilities ?? ['network']
47
+ this.commandExecutor = options.commandExecutor
35
48
  this.commandHandler = options.commandHandler
36
49
  }
37
50
 
@@ -43,18 +56,30 @@ export class RemoteExecutionContext extends BaseExecutionContext {
43
56
  })
44
57
  }
45
58
 
59
+ protected override onInitializationStarted(): void {
60
+ this.lifecycleAdmissionsOpen = false
61
+ }
62
+
63
+ protected override onInitializationCommitted(): void {
64
+ this.lifecycleAdmissionsOpen = true
65
+ }
66
+
67
+ protected override onTeardownRequested(): void {
68
+ this.lifecycleAdmissionsOpen = false
69
+ }
70
+
46
71
  protected async doTeardown(): Promise<void> {
47
- if (this.connected) {
48
- this.connected = false
49
- this.emit({
50
- type: 'remote_disconnected',
51
- contextId: this.id,
52
- host: this.target.host,
53
- })
54
- }
72
+ this.assertIdleForDisconnect()
73
+ await this.disconnect()
55
74
  }
56
75
 
57
76
  async connect(): Promise<void> {
77
+ if (!this.lifecycleAdmissionsOpen) {
78
+ throw new Error(
79
+ `Remote execution context "${this.id}" cannot connect until initialization commits after teardown or failed initialization.`,
80
+ )
81
+ }
82
+ if (this.connected) return
58
83
  this.connected = true
59
84
  this.emit({
60
85
  type: 'remote_connected',
@@ -70,6 +95,7 @@ export class RemoteExecutionContext extends BaseExecutionContext {
70
95
 
71
96
  async disconnect(): Promise<void> {
72
97
  if (!this.connected) return
98
+ this.assertIdleForDisconnect()
73
99
  this.connected = false
74
100
  this.emit({
75
101
  type: 'remote_disconnected',
@@ -100,20 +126,49 @@ export class RemoteExecutionContext extends BaseExecutionContext {
100
126
  return this.capabilities.includes(cap)
101
127
  }
102
128
 
129
+ setCommandExecutor(executor: CommandExecutor): void {
130
+ this.commandExecutor = executor
131
+ }
132
+
133
+ /** @deprecated Use `setCommandExecutor()`. */
103
134
  setCommandHandler(handler: RemoteCommandHandler): void {
104
135
  this.commandHandler = handler
105
136
  }
106
137
 
107
- async executeRemote(command: string, options?: CommandOptions): Promise<CommandResult> {
108
- if (!this.commandHandler) {
109
- throw new Error(
110
- `No remote command handler configured for context "${this.id}". Set one via setCommandHandler() before calling executeRemote().`,
138
+ async executeCommand(
139
+ command: string,
140
+ args: string[] = [],
141
+ options?: CommandOptions,
142
+ ): Promise<CommandResult> {
143
+ if (this.commandExecutor) {
144
+ const executor = this.admitCommand(
145
+ this.commandExecutor,
146
+ `No remote command executor configured for context "${this.id}". Set one via setCommandExecutor() before calling executeCommand().`,
111
147
  )
148
+ this.assertCancellationUnsupported(options?.signal)
149
+ return this.trackCommand(() => executor.executeCommand(command, args, options))
112
150
  }
113
- if (!this.connected) {
114
- throw new Error(`Remote context "${this.id}" is not connected. Call connect() first.`)
115
- }
116
- return this.commandHandler.executeRemote(command, options)
151
+
152
+ const handler = this.admitCommand(
153
+ this.commandHandler,
154
+ `No remote command executor configured for context "${this.id}". Set one via setCommandExecutor() before calling executeCommand().`,
155
+ )
156
+ this.assertCancellationUnsupported(options?.signal)
157
+ const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command
158
+ return this.trackCommand(() => handler.executeRemote(fullCommand, options))
159
+ }
160
+
161
+ /**
162
+ * @deprecated Use `executeCommand()` so command arguments retain their
163
+ * boundaries at Namzu's remote execution seam.
164
+ */
165
+ async executeRemote(command: string, options?: CommandOptions): Promise<CommandResult> {
166
+ const handler = this.admitCommand(
167
+ this.commandHandler,
168
+ `No remote command handler configured for context "${this.id}". Set one via setCommandHandler() before calling executeRemote().`,
169
+ )
170
+ this.assertCancellationUnsupported(options?.signal)
171
+ return this.trackCommand(() => handler.executeRemote(command, options))
117
172
  }
118
173
 
119
174
  toConfig(): RemoteExecutionContextConfig {
@@ -133,4 +188,54 @@ export class RemoteExecutionContext extends BaseExecutionContext {
133
188
  throw new Error(`Unsupported remote target type: "${target.type}"`)
134
189
  }
135
190
  }
191
+
192
+ private admitCommand<T>(implementation: T | undefined, missingMessage: string): T {
193
+ if (implementation === undefined) throw new Error(missingMessage)
194
+ if (!this.lifecycleAdmissionsOpen) {
195
+ throw new Error(
196
+ `Remote execution context "${this.id}" is initializing, tearing down, or torn down. Wait for initialization to commit before executing another command.`,
197
+ )
198
+ }
199
+ if (!this.connected) {
200
+ throw new Error(`Remote context "${this.id}" is not connected. Call connect() first.`)
201
+ }
202
+ return implementation
203
+ }
204
+
205
+ private assertCancellationUnsupported(signal: AbortSignal | undefined): void {
206
+ if (signal !== undefined) throw new CommandCancellationUnsupportedError(this.id)
207
+ }
208
+
209
+ private assertIdleForDisconnect(): void {
210
+ if (this.activeCommands.size > 0) {
211
+ throw new RemoteExecutionBusyError(this.id, this.activeCommands.size)
212
+ }
213
+ }
214
+
215
+ private trackCommand(invoke: () => Promise<CommandResult>): Promise<CommandResult> {
216
+ const ownership = Symbol('remote-command')
217
+ this.activeCommands.add(ownership)
218
+ const release = (): void => {
219
+ this.activeCommands.delete(ownership)
220
+ }
221
+
222
+ let operation: Promise<CommandResult>
223
+ try {
224
+ operation = Promise.resolve(invoke())
225
+ } catch (error) {
226
+ release()
227
+ return Promise.reject(error)
228
+ }
229
+
230
+ return operation.then(
231
+ (result) => {
232
+ release()
233
+ return result
234
+ },
235
+ (error: unknown) => {
236
+ release()
237
+ throw error
238
+ },
239
+ )
240
+ }
136
241
  }
@@ -711,6 +711,12 @@ export class PluginLifecycleManager {
711
711
  }
712
712
 
713
713
  const results: PluginHookResult[] = []
714
+ // Interrupt hooks are cleanup/notification observers. One extension's
715
+ // skip, error, retry, or timeout must not suppress the extensions that
716
+ // follow it, and none of those results can change the cancellation that
717
+ // already happened. Other hook events retain their existing flow-control
718
+ // semantics.
719
+ const observationalFanOut = event === 'run_interrupt'
714
720
 
715
721
  // Determine execution order: post_* hooks run backward (for cleanup semantics)
716
722
  const isPost = event.startsWith('post_')
@@ -830,16 +836,16 @@ export class PluginLifecycleManager {
830
836
 
831
837
  results.push(result)
832
838
 
833
- if (result.action === 'modify') {
839
+ if (!observationalFanOut && result.action === 'modify') {
834
840
  toolInputOverlay = result.input
835
841
  }
836
842
 
837
843
  // Handle flow control: check priority order: error > skip > retry > resume > modify > continue
838
844
  // Short-circuit on error or skip; return immediately on resume or retry
839
- if (result.action === 'error' || result.action === 'skip') {
845
+ if (!observationalFanOut && (result.action === 'error' || result.action === 'skip')) {
840
846
  break
841
847
  }
842
- if (result.action === 'retry') {
848
+ if (!observationalFanOut && result.action === 'retry') {
843
849
  break
844
850
  }
845
851
  }
@@ -10,23 +10,21 @@ import type { spawn } from 'node:child_process'
10
10
  * paid for by a bug where a cancelled command kept running, and a
11
11
  * near-copy would reproduce the bug in the copy.
12
12
  *
13
- * Every caller reaches a real command through `/bin/sh -c "cmd"`, and under
14
- * the local sandbox's `linux-namespace`/`macos-seatbelt` tiers that shell is
15
- * wrapped again (`unshare …`/`sandbox-exec -- /bin/sh -c "cmd"`). Either
16
- * way, `child.pid` names the OUTERMOST wrapper Node spawned, not `cmd` —
17
- * signalling only that pid, which is both what `child.kill()` does and what
18
- * `spawn`'s own `signal` option does internally on abort, reaps the wrapper
19
- * and leaves `cmd`, and anything it forked, running past both a cancel and a
20
- * timeout.
13
+ * Some callers reach a command through a shell or isolation wrapper; the
14
+ * local execution context can also spawn the authored executable directly.
15
+ * In every case, signalling only `child.pid` which is both what
16
+ * `child.kill()` does and what `spawn`'s own `signal`/`timeout` options do
17
+ * can reap that direct child while leaving a descendant running past both a
18
+ * cancel and a timeout.
21
19
  *
22
20
  * POSIX: the caller must spawn with `detached: true`, which makes the child
23
21
  * the leader of a new process group (pgid === pid) instead of joining this
24
22
  * Node process's own. A negative pid signals that whole group in one call.
25
- * Group membership is a host-kernel fact that a fork()'d descendant inherits
26
- * from its parent whether or not it or an ancestor such as `unshare --pid`
27
- * subsequently entered a new PID namespace, so this reaches `cmd` and its
28
- * descendants along with the wrapper itself even under the namespaced
29
- * isolation tiers.
23
+ * Group membership is a host-kernel fact that an ordinary forked descendant
24
+ * inherits from its parent, including under the namespaced isolation tiers.
25
+ * A process which deliberately starts a new session/process group can escape
26
+ * this boundary; this helper is process-group signalling, not a portable
27
+ * arbitrary-descendant ownership primitive.
30
28
  *
31
29
  * Windows has no process-group id to sign a kill with — `process.kill` with
32
30
  * a negative pid there either throws or silently does nothing, depending on
@@ -10,6 +10,8 @@ export interface ResolvedProviderCapabilities {
10
10
  supportsFunctionCalling: boolean
11
11
  supportsVision: boolean
12
12
  supportsDocuments: boolean
13
+ supportsToolResultImages: boolean
14
+ supportsToolResultDocuments: boolean
13
15
  maxOutputTokens?: number
14
16
  }
15
17
 
@@ -25,6 +27,8 @@ export const PERMISSIVE_PROVIDER_CAPABILITIES: ResolvedProviderCapabilities = {
25
27
  supportsFunctionCalling: true,
26
28
  supportsVision: true,
27
29
  supportsDocuments: true,
30
+ supportsToolResultImages: true,
31
+ supportsToolResultDocuments: true,
28
32
  }
29
33
 
30
34
  /**
@@ -46,6 +50,12 @@ export function resolveProviderCapabilities(
46
50
  supportsVision: declared?.supportsVision ?? PERMISSIVE_PROVIDER_CAPABILITIES.supportsVision,
47
51
  supportsDocuments:
48
52
  declared?.supportsDocuments ?? PERMISSIVE_PROVIDER_CAPABILITIES.supportsDocuments,
53
+ supportsToolResultImages:
54
+ declared?.supportsToolResultImages ??
55
+ PERMISSIVE_PROVIDER_CAPABILITIES.supportsToolResultImages,
56
+ supportsToolResultDocuments:
57
+ declared?.supportsToolResultDocuments ??
58
+ PERMISSIVE_PROVIDER_CAPABILITIES.supportsToolResultDocuments,
49
59
  maxOutputTokens: declared?.maxOutputTokens,
50
60
  }
51
61
  }