@akira-tl/forgerelay 0.3.5 → 0.3.7

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.
@@ -18,6 +18,60 @@ existing ID explicitly when the user wants to resume that logical workspace.
18
18
  A Git worktree directory is a separate physical workspace target from its source
19
19
  checkout, and each conversation can still have its own logical handle for it.
20
20
 
21
+ ### Bootstrap context and workspace inventory
22
+
23
+ Normal coding still starts with the shortest path:
24
+
25
+ ```text
26
+ open_workspace(path="~/project")
27
+ ```
28
+
29
+ The default `context="auto"` keeps the first useful bootstrap while avoiding
30
+ replay. ForgeRelay tracks delivered context by conversation plus canonical
31
+ workspace target and a content fingerprint, not by logical `workspaceId`. If the
32
+ same conversation opens or resumes another logical handle for the same physical
33
+ project and the fingerprint is unchanged, the response keeps only lightweight
34
+ workspace metadata. If loaded instruction contents or relevant Skill, Capability
35
+ guide, profile, diagnostic, or nested-instruction metadata changes, the next
36
+ automatic open returns the refreshed bootstrap.
37
+
38
+ Two explicit controls are available for exceptional cases:
39
+
40
+ ```text
41
+ open_workspace(workspaceId="ws_...", context="full")
42
+ open_workspace(workspaceId="ws_...", context="none")
43
+ ```
44
+
45
+ `full` forces a bootstrap refresh. `none` opens/resumes the workspace without
46
+ returning the full project context and does not record the current fingerprint as
47
+ already delivered. Context-delivery state is independent from logical-workspace
48
+ selection, so closing or switching one handle does not make the conversation
49
+ forget unchanged project context it already received.
50
+
51
+ Do not enumerate workspace state on every normal open. When the user wants to
52
+ continue an earlier task, choose among logical workspaces, or clean up accumulated
53
+ state, use the same Core tool in inventory mode:
54
+
55
+ ```text
56
+ open_workspace(action="list")
57
+ open_workspace(action="list", root="~/project")
58
+ open_workspace(action="list", staleOnly=true)
59
+ ```
60
+
61
+ Inventory is paginated, defaults to 50 entries, and caps each page at 100. It can
62
+ filter by `workspaceId`, persisted `status`, derived `state`, `mode`, canonical
63
+ root/source root, or stale-only state. Entries include a compact label such as
64
+ `project/ws_...`, checkout/worktree backing metadata, timestamps, idle duration,
65
+ root validity, and whether that logical workspace is currently selected by this
66
+ conversation. Listing is observational and does not refresh `lastUsedAt`.
67
+
68
+ Treat persisted status and derived state separately. `status="active"` means the
69
+ record has not been explicitly closed. A valid recent record has `state="active"`;
70
+ a valid active record idle for more than two days has `state="stale"`; an active
71
+ record whose root is missing or unusable has `state="invalid"`; and an explicitly
72
+ finalized persisted record has `state="closed"`. Ask the user before cleanup, then
73
+ use the existing `close_workspace` lifecycle on the selected `workspaceId`.
74
+
21
75
  ## Checkout-first behavior
22
76
 
23
77
  Checkout mode is the default:
@@ -103,10 +157,16 @@ CLAUDE.md
103
157
  CLAUDE.MD
104
158
  ```
105
159
 
106
- Nested project instruction files are returned as available paths rather than all
107
- being injected eagerly. Read the relevant nested file before working under that path.
108
- `FORGERELAY_AGENT_DIR` is not an instruction source; it remains only a compatibility
109
- skill-discovery path.
160
+ To keep broad workspaces such as `~` fast, initial nested-instruction discovery is
161
+ bounded to direct child directories instead of recursively walking the whole tree.
162
+ Deeper `AGENTS.md` / `CLAUDE.md` files are discovered lazily along a path the first
163
+ time the Agent accesses it, and already-scanned directories are cached for the life
164
+ of that workspace handle. A `read` result carries any newly discovered local
165
+ instructions before the requested file content. Side-effecting file tools and shell
166
+ commands discover instructions before execution; if new local instructions are
167
+ found, ForgeRelay returns them and requires the Agent to retry, so the side effect
168
+ does not occur before the relevant instructions are known. `FORGERELAY_AGENT_DIR`
169
+ is not an instruction source; it remains only a compatibility skill-discovery path.
110
170
 
111
171
  ## MCP capability loading
112
172
 
@@ -131,8 +191,9 @@ worktrees, subagents, artifact/change-review workflows, Host/OAuth/MCP App
131
191
  integration, and long-running shell/PTY/process behavior. Optional guides are
132
192
  advertised only when their feature is enabled; for example, disabled subagents
133
193
  and artifact/change-review features do not add those descriptors to bootstrap
134
- context. Reopening a workspace in the same Host context does not repeat the
135
- descriptors, but the previously advertised guides remain valid.
194
+ context. Bootstrap replay follows the conversation/canonical-target context
195
+ fingerprint described above, so changing logical `workspaceId` alone does not
196
+ repeat unchanged descriptors; previously advertised guides remain valid.
136
197
 
137
198
  The fingerprint is also a stale-Host-schema diagnostic. If `open_workspace`
138
199
  reports a capability such as `filesystem.rename-move` but the Host's current
@@ -223,12 +284,14 @@ Workspace IDs are logical conversation handles rather than physical-directory
223
284
  identities. The same conversation keeps a stable ID for a project, while another
224
285
  conversation normally receives a different ID pointing at the same checkout or
225
286
  worktree. `open_workspace` can explicitly resume a known `workspaceId`, and a
226
- fresh logical ID is created only when the user asks for one. When a project has
227
- other logical workspaces idle for more than two days, `open_workspace` reports
228
- all of them so the user can choose to resume or clean them up. `close_workspace`
229
- is the single public close operation: checkout-backed workspaces release the logical
230
- handle, while managed-worktree-backed workspaces require `commitMessage` and run the
231
- safe commit / fast-forward-only integration / cleanup lifecycle.
287
+ fresh logical ID is created only when the user asks for one. The normal open path
288
+ may still include `staleWorkspaces` as a passive reminder for same-target handles
289
+ idle for more than two days; use `open_workspace(action="list")` for complete,
290
+ filtered inventory when continuation or cleanup actually requires it.
291
+ `close_workspace` is the single public close operation: checkout-backed workspaces
292
+ release the logical handle, while managed-worktree-backed workspaces require
293
+ `commitMessage` and run the safe commit / fast-forward-only integration / cleanup
294
+ lifecycle.
232
295
 
233
296
  Shell commands are allowed to modify ordinary project files when that is a
234
297
  natural part of the user's requested development task; ForgeRelay does not apply
@@ -163,12 +163,43 @@ receives a different ID for the same physical checkout/worktree. Pass
163
163
  `workspaceId` to `open_workspace` to explicitly resume an existing handle in the
164
164
  current conversation. `newWorkspace: true` allocates a new logical handle without
165
165
  creating another checkout or Git worktree and should be used only on explicit user
166
- request. Logical workspaces idle for more than two days are returned in `staleWorkspaces` so
167
- the user can choose whether to resume or release them. `close_workspace` removes a
168
- checkout-backed logical handle without deleting checkout files. For a managed-worktree-backed
169
- workspace, `close_workspace` requires `commitMessage` and runs the existing safe
170
- worktree finalize lifecycle: close Hooks, commit when needed, fast-forward-only
171
- integration, cleanup, and alias invalidation.
166
+ request.
167
+
168
+ Bootstrap delivery is tracked separately from the selected logical workspace.
169
+ `open_workspace` defaults to `context="auto"`: ForgeRelay fingerprints the current
170
+ project context and returns the full AGENTS/Skills/Capability-guide/profile bootstrap
171
+ only when that conversation has not already received the current fingerprint for
172
+ the canonical workspace target. `context="full"` forces a refresh;
173
+ `context="none"` opens or resumes the logical workspace without returning the full
174
+ bootstrap and does not mark the current fingerprint as delivered. Closing or
175
+ switching a logical workspace therefore does not by itself cause unchanged project
176
+ context to be injected again, while changed context produces a new fingerprint and
177
+ is delivered on the next `auto` open.
178
+
179
+ Use `open_workspace(action="list")` only when the Agent needs to continue an older
180
+ logical workspace, choose among multiple handles, or organize workspace state. The
181
+ inventory is paginated (50 records by default, at most 100) and can filter by
182
+ workspace ID, persisted status, derived state, mode, canonical root/source root, or
183
+ stale-only state. Reading inventory does not refresh `lastUsedAt`. Persisted
184
+ `status="active"` means the record has not been explicitly closed; the derived
185
+ `state` distinguishes `active`, `stale`, `invalid`, and `closed`. A missing checkout
186
+ or externally removed managed-worktree root can therefore remain diagnostically
187
+ `status="active"` while appearing as `state="invalid"`. The existing
188
+ `staleWorkspaces` field remains a passive same-workspace reminder for old handles;
189
+ `action="list"` is the formal on-demand inventory path.
190
+
191
+ `close_workspace` removes a checkout-backed logical handle without deleting checkout
192
+ files. For a managed-worktree-backed workspace, `close_workspace` requires
193
+ `commitMessage` and runs the existing safe worktree finalize lifecycle: close Hooks,
194
+ commit when needed, fast-forward-only integration, cleanup, and alias invalidation.
195
+
196
+ Hot workspace/session activity timestamps are coalesced in memory and flushed to the
197
+ SQLite state database in a transaction at most every five minutes; normal shutdown
198
+ performs a final explicit flush. Reads within the running ForgeRelay process see the
199
+ latest in-memory timestamps immediately. Workspace creation, close/status changes,
200
+ context-delivery checkpoints, and other semantic state transitions remain immediate
201
+ persistent writes. A hard process crash may therefore lose only the most recent
202
+ activity timestamp window, not the existence or closed/open state of a workspace.
172
203
 
173
204
  Regular `bash` has no execution-timeout input. `action="run"` (the default) waits
174
205
  in the foreground for at most 300 seconds; if the process is still alive, the
@@ -176,7 +207,10 @@ result contains `running: true` and a canonical `processId`. Reuse the same `bas
176
207
  with `action="process"` to poll/wait, send `input`, resize a PTY, or set
177
208
  `interrupt:true`; each wait can be up to 300 seconds. ForgeRelay does not kill a
178
209
  process merely because a wait window expires. Completed background processes are
179
- delivered once with a later tool result for the same logical workspace ID.
210
+ delivered once with a later tool result for the same logical workspace ID. An
211
+ unconsumed completed-process notice is retained for at most five minutes, and
212
+ ForgeRelay also bounds active and completed process counts so repeated background
213
+ commands cannot grow server memory without limit.
180
214
 
181
215
  Codex mode retains `write_stdin` only as an experimental compatibility adapter;
182
216
  regular Agent workflows should use the single `bash` process lifecycle.
@@ -325,8 +359,12 @@ Arrays or empty values are not accepted. Symbolic links are followed, so the
325
359
  runtime entry may point at a canonical source elsewhere on disk.
326
360
 
327
361
  Project-root `AGENTS.md` / `CLAUDE.md` files remain project context and are
328
- loaded separately. `FORGERELAY_AGENT_DIR` does not select a global instruction
329
- file; it remains a compatibility path for Agent Skills.
362
+ loaded separately. Initial nested-instruction discovery checks only direct child
363
+ directories; deeper instruction files are discovered lazily when a workspace path
364
+ is first accessed. Reads surface newly discovered instructions inline, while
365
+ side-effecting file/shell operations stop before execution and require a retry if
366
+ that access discovers new local instructions. `FORGERELAY_AGENT_DIR` does not
367
+ select a global instruction file; it remains a compatibility path for Agent Skills.
330
368
 
331
369
  ## Skills and subagents
332
370
 
package/docs/roadmap.md CHANGED
@@ -170,7 +170,29 @@ capability
170
170
  - 删除已经完成迁移的 dedicated low-frequency tool aliases,确保新增 Capability 不再扩大常驻 tool count;
171
171
  - 简化 fingerprint,使其用于版本/运行时能力摘要与 stale-Host 诊断,而不是重新枚举 tool implementation;
172
172
  - 对 `open_workspace → catalog → capability describe/read/run`、managed worktree close、长进程 interaction、review/artifact capability、MCP App 与 stale-schema 情况做 7677 acceptance 和新 Host 会话验收;
173
- - 0.3.5 通过后,0.3 的 MCP progressive-disclosure 主题视为完成,0.4 回到原定 LSP code intelligence v1。
173
+ - 0.3.5 完成 canonical MCP surface 与 fresh-Host 主验收后,0.3 的 progressive-disclosure 主体设计视为稳定;后续 0.3.x 只接收验收暴露出的兼容性或 lifecycle 补丁,0.4 仍回到原定 LSP code intelligence v1。
174
+
175
+ ### 0.3.6 — Workspace bootstrap 与 inventory 补丁
176
+
177
+ 0.3.6 处理 0.3.5 fresh-Host 验收后暴露的 Workspace 上下文与 logical-workspace 管理问题,不增加新的常驻 MCP tool,也不改变 0.4 的 LSP 主路线:
178
+
179
+ - `open_workspace` 的 bootstrap 去重从 logical `workspaceId` 身份提升为 conversation scope + canonical workspace target + context fingerprint;同一 conversation 切换到同一物理 checkout/worktree 的其他 logical workspace 时,只要 AGENTS、Skills、Capability guide/profile 等相关上下文没有变化,就不重复注入完整 bootstrap;
180
+ - `open_workspace` 提供 `context="auto" | "full" | "none"`。`auto` 默认只在当前 fingerprint 尚未交付或已变化时返回完整上下文,`full` 强制刷新,`none` 只取得 workspace handle/metadata 且不会把该 fingerprint 标记为已交付;
181
+ - context-delivery 状态与 conversation 当前绑定的 logical workspace 分离持久化,因此关闭或切换 logical handle 不会让 Agent 在同一 conversation 中忘记已经收到的项目上下文;delivery 状态继续服从有限生命周期清理,而不是永久缓存;
182
+ - `open_workspace(action="list")` 成为按需 logical-workspace inventory 入口,不增加 `list_workspaces`/`workspace.list` 第十个 Core tool;普通开发仍直接使用默认 `action="open"`,只有续接旧任务、选择 workspace 或整理状态时才读取 inventory;
183
+ - inventory 区分持久化 `status` 与派生 `state`:`status="active"` 表示尚未显式关闭,`state` 再区分 active、stale、invalid 与 closed;missing root 或外部删除的 managed worktree 可以保持可诊断的 active record,同时显示为 invalid;
184
+ - inventory 查看本身不刷新 workspace `lastUsedAt`,支持过滤与分页,并继续让现有 `close_workspace` 承担用户确认后的实际清理/finalize lifecycle。
185
+
186
+ ### 0.3.7 — 资源生命周期与 Workspace I/O 性能补丁
187
+
188
+ 0.3.7 处理长时间运行实例在高请求量、高输出 `bash`、大量 logical workspace 与宽根目录下的资源放大问题,不改变 canonical 9-tool surface:
189
+
190
+ - completed background process 的 Agent 可消费状态最多保留 5 分钟;底层 ChildProcess/PTY handle 在退出时立即释放,completed notice 数量与 active process 数量都有硬上限,并缩小单 process 输出驻留预算;
191
+ - 高输出 head/tail buffer 保留 Unicode code-point 语义,但不再通过 `Array.from(整段输出)` 构造巨型临时数组,降低 V8 heap 扩容与 GC 压力;
192
+ - MCP transport registry 与 review checkpoint state 加入容量边界;正常 transport close/workspace close 仍立即释放,异常遗弃对象不能再无限累积;OAuth 过期 authorization code 也会主动淘汰;
193
+ - `open_workspace` 的 instruction discovery 首轮只检查 root 与直接子目录,不再递归整棵 workspace。更深层 `AGENTS.md` / `CLAUDE.md` 在 Agent 首次访问对应路径时沿祖先目录惰性发现,并缓存已扫描目录;read 可直接携带新发现指令,write/edit/rename/delete/bash 等副作用调用则在执行前返回指令并要求重试;
194
+ - Workspace SQLite 继续作为本地持久化真源,不引入 Redis/PostgreSQL/Docker。高频 session/conversation `lastUsedAt` touch 进入内存 write-behind cache,最多每 5 分钟事务批量 flush,normal shutdown 再显式 flush;create/close/status 等语义性状态仍同步持久化;
195
+ - debug runtime telemetry 定期报告 RSS/heap、transport、process、workspace cache 与 review state 数量,为后续真实实例资源趋势提供可观测性。
174
196
 
175
197
  必要安全语义始终留在 Core tool interface、Capability contract 或自动 Hook report 中;渐进式披露不能成为隐藏权限、隐式 autonomous workflow 或绕过 allowed roots/auth 的机制。`rename` 继续作为文件和目录 move/rename 的统一 primitive。
176
198
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akira-tl/forgerelay",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "description": "Local development control plane for MCP coding agents.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Akira-TL/forgerelay#readme",
@@ -42,7 +42,7 @@
42
42
  "debug:accept": "node scripts/debug/accept.mjs",
43
43
  "postinstall": "node scripts/fix-node-pty-permissions.mjs",
44
44
  "start": "node dist/cli.js serve",
45
- "test": "tsx src/config.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
45
+ "test": "tsx src/config.test.ts && tsx src/logger.test.ts && tsx src/proxy-trust.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/capability-registry.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspace-store.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
46
46
  "typecheck": "tsc -p tsconfig.json --noEmit",
47
47
  "release:check": "node scripts/release-version.mjs check",
48
48
  "release:tag-check": "node scripts/release-version.mjs tag",
@@ -223,13 +223,16 @@ try {
223
223
  assert.equal(historicalTemplate.text, template.text);
224
224
  pass("MCP app template", `${templateUri} + legacy/history compatibility -> ${scriptUrl}`);
225
225
 
226
+ const workspaceConversationMeta = { "openai/session": "acceptance-workspace" };
226
227
  const opened = callTool(oauth.accessToken, sessionId, 3, "open_workspace", {
227
228
  path: checkoutWorkspace,
228
- });
229
+ }, workspaceConversationMeta);
229
230
  const workspaceId = opened.structuredContent.workspaceId;
230
231
  assert.match(workspaceId, /^ws_/);
232
+ assert.equal(opened.structuredContent.action, "open");
231
233
  assert.equal(opened.structuredContent.root, checkoutWorkspace);
232
234
  assert.equal(opened.structuredContent.mode, "checkout");
235
+ assert.equal(typeof opened.structuredContent.contextFingerprint, "string");
233
236
  assert.deepEqual(opened.structuredContent.capabilityFingerprint, {
234
237
  version: packageJson.version,
235
238
  toolMode: "full",
@@ -254,6 +257,56 @@ try {
254
257
  ]);
255
258
  assert.equal(capabilityCatalog[0].available, true);
256
259
  assert.equal(capabilityCatalog[0].guide.name, "lifecycle-hooks");
260
+
261
+ const freshLogical = callTool(oauth.accessToken, sessionId, 84, "open_workspace", {
262
+ path: checkoutWorkspace,
263
+ newWorkspace: true,
264
+ }, workspaceConversationMeta);
265
+ const freshLogicalId = freshLogical.structuredContent.workspaceId;
266
+ assert.notEqual(freshLogicalId, workspaceId);
267
+ assert.equal(freshLogical.structuredContent.action, "open");
268
+ assert.equal(
269
+ freshLogical.structuredContent.contextFingerprint,
270
+ opened.structuredContent.contextFingerprint,
271
+ );
272
+ assert.equal(freshLogical.structuredContent.agentsFiles, undefined);
273
+ assert.equal(freshLogical.structuredContent.capabilityGuides, undefined);
274
+
275
+ const workspaceInventory = callTool(oauth.accessToken, sessionId, 85, "open_workspace", {
276
+ action: "list",
277
+ root: checkoutWorkspace,
278
+ }, workspaceConversationMeta);
279
+ assert.equal(workspaceInventory.structuredContent.action, "list");
280
+ assert.equal(workspaceInventory.structuredContent.summary.matching, 2);
281
+ const inventoryEntries = workspaceInventory.structuredContent.workspaces;
282
+ assert.equal(inventoryEntries.length, 2);
283
+ assert.equal(
284
+ inventoryEntries.find((entry) => entry.workspaceId === freshLogicalId)?.current,
285
+ true,
286
+ );
287
+ assert.equal(
288
+ inventoryEntries.find((entry) => entry.workspaceId === workspaceId)?.current,
289
+ false,
290
+ );
291
+
292
+ const closedFreshLogical = callTool(oauth.accessToken, sessionId, 86, "close_workspace", {
293
+ workspaceId: freshLogicalId,
294
+ });
295
+ assert.equal(closedFreshLogical.isError, undefined);
296
+ const resumedOriginal = callTool(oauth.accessToken, sessionId, 87, "open_workspace", {
297
+ workspaceId,
298
+ }, workspaceConversationMeta);
299
+ assert.equal(resumedOriginal.structuredContent.workspaceId, workspaceId);
300
+ assert.equal(resumedOriginal.structuredContent.agentsFiles, undefined);
301
+ assert.equal(
302
+ resumedOriginal.structuredContent.contextFingerprint,
303
+ opened.structuredContent.contextFingerprint,
304
+ );
305
+ pass(
306
+ "workspace context + inventory",
307
+ `${workspaceId} -> ${freshLogicalId} -> list -> close -> resume without bootstrap replay`,
308
+ );
309
+
257
310
  const directCapability = callTool(oauth.accessToken, sessionId, 79, "capability", {
258
311
  workspaceId,
259
312
  name: "hooks.check",
@@ -590,12 +643,16 @@ function mcpRequest(accessToken, sessionId, request) {
590
643
  return { response, message: parseMcpMessage(response.body, request.id) };
591
644
  }
592
645
 
593
- function callTool(accessToken, sessionId, id, name, args) {
646
+ function callTool(accessToken, sessionId, id, name, args, meta) {
594
647
  const message = mcpRequest(accessToken, sessionId, {
595
648
  jsonrpc: "2.0",
596
649
  id,
597
650
  method: "tools/call",
598
- params: { name, arguments: args },
651
+ params: {
652
+ name,
653
+ arguments: args,
654
+ ...(meta ? { _meta: meta } : {}),
655
+ },
599
656
  }).message;
600
657
  assert.equal(message.id, id);
601
658
  assert.ok(message.result, `tool ${name} did not return a result`);