Skip to content

Commit 3b920f6

Browse files
fix(mcp): update_record and delete_record answer a write that landed under a failing roll-up with their own success (#28854)
domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt <!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X8dNy8r76WjFsVdvaSJbXXR)_ Fixes #28844 Clause-②: no Metadata-protocol: none Skills-docs: none — no file under `skills/**` teaches what an MCP write tool answers when a parent roll-up cannot be recomputed after the write. ## 维护者速读 - 问题:子记录的更新或删除已经生效、只是父记录汇总重算失败(`ERR_SUMMARY_RECOMPUTE`)时,MCP 的 `update_record` / `delete_record` 仍返回错误结果,agent 会以为写入失败而重做一次。#28843 只修了 `create_record`。 - 现在:两个工具在两种 bridge 上都返回各自已有的成功结果(更新 `{ object, id, record }`,删除 `{ object, id, success: true }`),复用 #28781 的载体和那条 stale-summary 日志;没有新错误码、字段或形状。 - 写入前的拒绝(校验、权限、找不到、暴露设置)完全照旧;只改 `packages/mcp`,未改 `packages/spec/**`。 - `run_action` 未改:动作的结果不是这次写入的答案,载体里也没有它,需要另行裁定(见 Acceptance notes)。 ## What changes `packages/mcp` only, through #28781's carrier in `@objectstack/types` (`earnedAnswerOf` / `staleSummaryAnswerLines` / `staleSummaryRefusalOrThrow` / `withEarnedAnswer`). - **Tools read the answer** (`mcp-http-tools.ts`): `create_record`'s #28843 reading moves into one local helper, `landedWriteResultOrError(err, objectName)`, and `update_record` and `delete_record` call it from their catches. It answers the attached answer as a normal `textResult` and logs the stale column at `error` (`[MCP]` prefix, once per field per process); any other throw renders through `errorResultWithCode` as before. The carrier is read by `code` inline in `@objectstack/types`; no new constant. - **Runtime bridge**: no change needed. `update` / `remove` reach the protocol's `updateData` / `deleteData` through `callData`, and those attach the answer since #28781. - **stdio bridge** (`stdio-data-bridge.ts`): `update` and `remove` wrap `engine.update` / `engine.delete` the way `create` wraps `engine.insert` (#28843). `staleSummaryRefusalOrThrow` rethrows every other error unchanged; on the stale refusal the bridge builds the same answer it returns on success and throws `withEarnedAnswer(stale, answer)`, so a caller that does not read the carrier still gets the refusal. ## Tests `packages/mcp/src/mcp-update-delete-record-stale-summary.test.ts` (5, over the HTTP `/mcp` transport via `MCPServerRuntime.handleHttpRequest`). The engine is a double over a row map whose `update` / `delete` apply the write, then throw the refusal as objectql's `SummaryRecomputeError` does; it routes through the engine-dispatch asserts (`check:engine-double-contract` ledger updated). - `update_record`, stdio bridge: `{ object, id, record }` with the patched row, update applied once. - `delete_record`, stdio bridge: `{ object, id, success: true }`, row gone, delete applied once. - Both tools, runtime-bridge shape: relays the answer the protocol attached. - Pre-write guard: an engine refusal before the write (`VALIDATION_FAILED`) is still `isError` with its code, nothing written. - One `error` log line naming `"qsf_parent.probe_sf"`. Red on `origin/main` `b95a856e3` (test file only): 4 red, the pre-write guard green. Green on the head. Ablations, each landed on disk then restored: | ablation | red | |---|---| | `update_record` catch back to `errorResultWithCode` | 2 (stdio update, runtime-bridge) | | `delete_record` catch back to `errorResultWithCode` | 2 (stdio delete, runtime-bridge) | | stdio `update` throws the bare refusal | 1 (stdio update) | | stdio `remove` throws the bare refusal | 1 (stdio delete) | | stdio `update` answers success for every engine throw | 1 (pre-write guard) | `@objectstack/mcp` suite: 52 files, 640/640. ## Acceptance notes - **No result shape change.** Each tool answers the result it already publishes for the same write when every roll-up recomputes; no published type changed. - **`run_action` is not included.** Its answer is the action's own result (`invokeBusinessAction` in `packages/runtime`), not the write's data answer the carrier holds, and the REST action door does not read the carrier either. Same symptom, different reading; it needs its own ruling and a runtime-side change. - No `packages/spec/**` edit; `check:error-code-provenance` exit 0. ## Verification Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 67 commands on the diff; all 67 exit 0, and `--ran` reconciles 67 derived, 67 run, 0 NOT-MEASURED. `check:engine-double-contract` first read exit 1 on the new double (unrecorded in the pinned ledger), fixed by `--write` and committing `scripts/engine-double-contract.pinned.json`. `pnpm --filter @objectstack/spec check:error-code-provenance`: exit 0. `pnpm --filter @objectstack/mcp typecheck` (includes `check:test-typecheck`): exit 0. Scoped eslint over the three changed `.ts` files: exit 0. After the final merge of `main` (`a3143182`, nothing under `packages/mcp` or `packages/types`): `check:engine-double-contract` and `check:error-code-provenance` exit 0, the new file, `create_record`'s #28843 file and the stdio bridge suites green (9 files, 153 tests). No live probe: the stdio bridge runs the engine directly and the runtime bridge's producer half (`updateData` / `deleteData`) is #28781's, pinned on a real store by its REST suite; this change is the tools' read and the stdio carrier, both pinned above. Full dogfood not owed (no `!`, no narrowing arm). --- _Generated by [Claude Code](https://claude.ai/code/session_01FEkQ5kV6ptpTnU6Hjb2mE4)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7e60dc2 commit 3b920f6

5 files changed

Lines changed: 280 additions & 15 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/mcp': patch
3+
---
4+
5+
fix(mcp): `update_record` and `delete_record` answer a write that landed under a failing roll-up with their own success (#28844)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
Skills-docs: none — no file under `skills/**` teaches what an MCP write tool answers when a parent roll-up cannot be recomputed after the write.
10+
11+
When a child update or delete is applied but a parent's roll-up summary cannot be recomputed after it (`ERR_SUMMARY_RECOMPUTE`, "the triggering records WERE written"), the MCP `update_record` and `delete_record` tools now answer the success result they already publish, the way #28782 made `create_record` answer: `{ object, id, record }` for an update and `{ object, id, success: true }` for a delete, instead of an `isError` result that told an agent its write failed and invited it to apply it again. No new code, member or shape.
12+
13+
- **Runtime bridge**: the tools relay the answer the protocol's `updateData` / `deleteData` already attach to the refusal (#28781).
14+
- **stdio bridge**: `update` and `remove` attach their own answer to the refusal, as its `create` does.
15+
16+
The stale summary column is logged at `error` once per field per process, with the fix. A refusal before the write (validation, permission, not found, exposure) answers exactly as before. `run_action` is unchanged.

‎packages/mcp/src/mcp-http-tools.ts‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,22 @@ function errorResultWithCode(err: unknown) {
481481
return envelope ? codedErrorResult({ ...envelope }) : errorResult(messageOf(err));
482482
}
483483

484+
/**
485+
* [#28782, #28844] A write tool's answer to a throw: a write the engine stored
486+
* but whose parent roll-up it could not recompute (`ERR_SUMMARY_RECOMPUTE`,
487+
* "the triggering records WERE written") answers the write it is, read off
488+
* #28781's carrier the bridge attached, with the stale column logged once at
489+
* `error`. An agent that read an error would apply the write again. Any other
490+
* throw, including every refusal raised before the write, renders through
491+
* {@link errorResultWithCode} exactly as before.
492+
*/
493+
function landedWriteResultOrError(err: unknown, objectName: string) {
494+
const earned = earnedAnswerOf(err);
495+
if (earned === undefined) return errorResultWithCode(err);
496+
for (const line of staleSummaryAnswerLines(err as SummaryRecomputeRefusal, objectName)) console.error(`[MCP] ${line}`);
497+
return textResult(earned);
498+
}
499+
484500
function codedErrorResult(error: ThrownRefusalEnvelope & { details?: unknown }) {
485501
return {
486502
content: [{ type: 'text' as const, text: jsonText({ error }) }],
@@ -1332,15 +1348,7 @@ export function registerObjectTools(
13321348
try {
13331349
return textResult(await bridge.create(objectName, data));
13341350
} catch (err) {
1335-
// [#28782] A record the engine stored but whose parent roll-up it
1336-
// could not recompute answers the create it is (#28781's carrier),
1337-
// not the refusal: an agent that read an error would create it again.
1338-
const earned = earnedAnswerOf(err);
1339-
if (earned) {
1340-
for (const line of staleSummaryAnswerLines(err as SummaryRecomputeRefusal, objectName)) console.error(`[MCP] ${line}`);
1341-
return textResult(earned);
1342-
}
1343-
return errorResultWithCode(err);
1351+
return landedWriteResultOrError(err, objectName);
13441352
}
13451353
},
13461354
);
@@ -1374,7 +1382,7 @@ export function registerObjectTools(
13741382
try {
13751383
return textResult(await bridge.update(objectName, recordId, data));
13761384
} catch (err) {
1377-
return errorResultWithCode(err);
1385+
return landedWriteResultOrError(err, objectName);
13781386
}
13791387
},
13801388
);
@@ -1398,7 +1406,7 @@ export function registerObjectTools(
13981406
try {
13991407
return textResult(await bridge.remove(objectName, recordId));
14001408
} catch (err) {
1401-
return errorResultWithCode(err);
1409+
return landedWriteResultOrError(err, objectName);
14021410
}
14031411
},
14041412
);
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#28844] The MCP `update_record` and `delete_record` tools answer a write the
5+
* engine STORED but whose parent roll-up it could not recompute with the
6+
* update or delete it is — never the refusal, which told an agent its write
7+
* failed and invited it to apply it again.
8+
*
9+
* ## The defect, measured on `origin/main` (`b95a856e3`) before this file
10+
*
11+
* The engine reports that case as `ERR_SUMMARY_RECOMPUTE` ("the triggering
12+
* records WERE written", carrying `written`). #28843 taught `create_record`
13+
* and the stdio bridge's `create` to read #28781's carrier; the update and
14+
* delete tools still rendered the refusal as an `isError` result on both
15+
* bridges:
16+
*
17+
* - the runtime bridge (`callData('update' | 'delete')` → the protocol's
18+
* `updateData` / `deleteData`, which attach the earned answer since #28781)
19+
* — the tools never read it;
20+
* - the stdio bridge (`createStdioDataBridge`, `engine.update` /
21+
* `engine.delete` direct) built no answer at all.
22+
*
23+
* ## What is pinned
24+
*
25+
* - over the HTTP `/mcp` transport, each tool answers the bridge's success
26+
* result for both bridges, and the stdio bridge applies the write once;
27+
* - a refusal BEFORE the write is still an `isError` result (ruling 4);
28+
* - the stale column is logged once, at `error`.
29+
*
30+
* The engine is a double whose `update` / `delete` apply the write and then
31+
* throw the refusal as objectql's `SummaryRecomputeError` does (this package
32+
* has no SQL driver); the real engine's throw is pinned by #28781's REST suite.
33+
*/
34+
35+
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
36+
import type { IDataEngine, IMetadataService } from '@objectstack/spec/contracts';
37+
import type { ExecutionContext } from '@objectstack/spec/kernel';
38+
import { withEarnedAnswer } from '@objectstack/types';
39+
import { assertEngineDeleteDispatch, assertEngineUpdateDispatch, assertEngineFindOnePredicate, type EngineFindOneQueryInput } from '@objectstack/metadata-core';
40+
41+
import { MCPServerRuntime } from './mcp-server-runtime.js';
42+
import { createStdioDataBridge } from './stdio-data-bridge.js';
43+
import type { McpDataBridge } from './mcp-http-tools.js';
44+
45+
const CHILD = {
46+
name: 'qsf_child',
47+
label: 'Child',
48+
fields: {
49+
id: { name: 'id', type: 'text' },
50+
name: { name: 'name', type: 'text' },
51+
amount: { name: 'amount', type: 'number' },
52+
},
53+
enable: {},
54+
};
55+
56+
/** objectql's `SummaryRecomputeError`, as a write's caller reads it: by `code`, carrying `written`. */
57+
function staleRefusal(written: unknown): Error {
58+
return Object.assign(new Error('Roll-up summary recompute failed after retries for 1 parent record(s); the triggering records WERE written.'), {
59+
code: 'ERR_SUMMARY_RECOMPUTE',
60+
written,
61+
failures: [{ childObject: 'qsf_child', parentObject: 'qsf_parent', parentId: 'p1', field: 'probe_sf', error: new Error('sum over text') }],
62+
});
63+
}
64+
65+
const idOf = (options: any): string | undefined => options?.where?.id;
66+
67+
/**
68+
* A store of `qsf_child` rows behind an engine whose `update` / `delete` apply
69+
* the write, then report the stale roll-up — or refuse before writing anything
70+
* when the patch names `refused`.
71+
*/
72+
function engineDouble(rows: Map<string, Record<string, unknown>>, writes: string[]) {
73+
return {
74+
find: vi.fn(async (_o: string, query?: any) => {
75+
const id = query?.where?.id;
76+
const all = [...rows.values()].filter((r) => id === undefined || r.id === id);
77+
return typeof query?.limit === 'number' ? all.slice(0, query.limit) : all;
78+
}),
79+
findOne: vi.fn(async (object: string, query?: EngineFindOneQueryInput) => {
80+
assertEngineFindOnePredicate(object, query);
81+
return rows.get(String((query as any)?.where?.id)) ?? null;
82+
}),
83+
insert: vi.fn(async () => { throw new Error('not used'); }),
84+
update: vi.fn(async (_o: string, data: Record<string, unknown>, options?: any) => {
85+
assertEngineUpdateDispatch(data, options);
86+
if (data.name === 'refused') {
87+
throw Object.assign(new Error('name is not allowed'), { code: 'VALIDATION_FAILED', status: 400 });
88+
}
89+
const id = idOf(options)!;
90+
const next = { ...rows.get(id), ...data };
91+
rows.set(id, next);
92+
writes.push(`update:${id}`);
93+
throw staleRefusal(next);
94+
}),
95+
delete: vi.fn(async (_o: string, options?: any) => {
96+
assertEngineDeleteDispatch(options);
97+
const id = idOf(options)!;
98+
rows.delete(id);
99+
writes.push(`delete:${id}`);
100+
throw staleRefusal(true);
101+
}),
102+
count: vi.fn(async () => rows.size),
103+
} as unknown as IDataEngine;
104+
}
105+
106+
function metadataDouble(): IMetadataService {
107+
return {
108+
listObjects: vi.fn(async () => [CHILD]),
109+
getObject: vi.fn(async () => CHILD),
110+
} as unknown as IMetadataService;
111+
}
112+
113+
function stdioBridge(rows: Map<string, Record<string, unknown>>, writes: string[]) {
114+
return createStdioDataBridge({
115+
engine: engineDouble(rows, writes),
116+
metadataService: metadataDouble(),
117+
resolvePrincipal: async () => ({ userId: 'u1' }) as unknown as ExecutionContext,
118+
});
119+
}
120+
121+
let nextId = 1;
122+
async function callTool(bridge: McpDataBridge, name: string, args: Record<string, unknown>) {
123+
const body = { jsonrpc: '2.0', id: nextId++, method: 'tools/call', params: { name, arguments: args } };
124+
const runtime = new MCPServerRuntime({ name: 'pin', version: '0.0.0' });
125+
const res = await runtime.handleHttpRequest(
126+
new Request('http://localhost/api/v1/mcp', {
127+
method: 'POST',
128+
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
129+
body: JSON.stringify(body),
130+
}),
131+
{ bridge: bridge as any, parsedBody: body },
132+
);
133+
expect(res.status).toBe(200);
134+
const json = (await res.json()) as { result?: any; error?: unknown };
135+
expect(json.error, JSON.stringify(json.error)).toBeUndefined();
136+
return json.result as { content: Array<{ text: string }>; isError?: boolean };
137+
}
138+
139+
/** The runtime bridge's shape: `callData` hands back the protocol's refusal with the earned answer attached. */
140+
function runtimeShapedBridge(answers: { update: Record<string, unknown>; remove: Record<string, unknown> }): McpDataBridge {
141+
return {
142+
async listObjects() { return [{ name: 'qsf_child', label: 'Child', fieldCount: 3 }]; },
143+
async describeObject() { return { name: 'qsf_child', fields: [] }; },
144+
async query(object: string) { return { object, records: [], total: 0 }; },
145+
async get() { return null; },
146+
async create() { return null; },
147+
async update() { throw withEarnedAnswer(staleRefusal(answers.update.record) as any, answers.update); },
148+
async remove() { throw withEarnedAnswer(staleRefusal(true) as any, answers.remove); },
149+
} as unknown as McpDataBridge;
150+
}
151+
152+
let errorLog: ReturnType<typeof vi.spyOn>;
153+
beforeAll(() => { errorLog = vi.spyOn(console, 'error').mockImplementation(() => {}); });
154+
afterAll(() => { errorLog.mockRestore(); });
155+
156+
describe('[#28844] MCP update_record / delete_record answer a write applied under a failing roll-up as the write it is', () => {
157+
it('update_record, stdio bridge: the success result, the update applied once', async () => {
158+
const rows = new Map([['c1', { id: 'c1', name: 'c1', amount: 100 }]]);
159+
const writes: string[] = [];
160+
const result = await callTool(stdioBridge(rows, writes), 'update_record', { objectName: 'qsf_child', recordId: 'c1', data: { amount: 35 } });
161+
expect(result.isError, result.content[0]?.text).toBeFalsy();
162+
expect(JSON.parse(result.content[0]!.text)).toEqual({ object: 'qsf_child', id: 'c1', record: { id: 'c1', name: 'c1', amount: 35 } });
163+
expect(writes).toEqual(['update:c1']);
164+
expect(rows.get('c1')).toMatchObject({ amount: 35 });
165+
});
166+
167+
it('delete_record, stdio bridge: the success result, the delete applied once', async () => {
168+
const rows = new Map([['c2', { id: 'c2', name: 'c2', amount: 135 }]]);
169+
const writes: string[] = [];
170+
const result = await callTool(stdioBridge(rows, writes), 'delete_record', { objectName: 'qsf_child', recordId: 'c2' });
171+
expect(result.isError, result.content[0]?.text).toBeFalsy();
172+
expect(JSON.parse(result.content[0]!.text)).toEqual({ object: 'qsf_child', id: 'c2', success: true });
173+
expect(writes).toEqual(['delete:c2']);
174+
expect(rows.has('c2')).toBe(false);
175+
});
176+
177+
it('both tools, runtime bridge: the answer the protocol attached to the refusal', async () => {
178+
const answers = {
179+
update: { object: 'qsf_child', id: 'r1', record: { id: 'r1', name: 'r1', amount: 6 } },
180+
remove: { object: 'qsf_child', id: 'r2', success: true },
181+
};
182+
const bridge = runtimeShapedBridge(answers);
183+
const updated = await callTool(bridge, 'update_record', { objectName: 'qsf_child', recordId: 'r1', data: { amount: 6 } });
184+
expect(updated.isError, updated.content[0]?.text).toBeFalsy();
185+
expect(JSON.parse(updated.content[0]!.text)).toEqual(answers.update);
186+
const removed = await callTool(bridge, 'delete_record', { objectName: 'qsf_child', recordId: 'r2' });
187+
expect(removed.isError, removed.content[0]?.text).toBeFalsy();
188+
expect(JSON.parse(removed.content[0]!.text)).toEqual(answers.remove);
189+
});
190+
191+
it('a refusal before the write is still an error result, and nothing is written', async () => {
192+
const rows = new Map([['c3', { id: 'c3', name: 'c3', amount: 10 }]]);
193+
const writes: string[] = [];
194+
const result = await callTool(stdioBridge(rows, writes), 'update_record', { objectName: 'qsf_child', recordId: 'c3', data: { name: 'refused' } });
195+
expect(result.isError).toBe(true);
196+
expect(result.content[0]!.text).toContain('VALIDATION_FAILED');
197+
expect(writes).toEqual([]);
198+
expect(rows.get('c3')).toMatchObject({ name: 'c3' });
199+
});
200+
201+
it('the stale column is logged once, at error', () => {
202+
const lines = errorLog.mock.calls.map((c: unknown[]) => String(c[0])).filter((l: string) => l.includes('could not be recomputed'));
203+
expect(lines).toHaveLength(1);
204+
expect(lines[0]).toContain('"qsf_parent.probe_sf"');
205+
});
206+
});

‎packages/mcp/src/stdio-data-bridge.ts‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,7 +1105,16 @@ export function createStdioDataBridge(deps: StdioDataBridgeDeps): McpDataBridge
11051105
// `Error`, so a stdio caller sees the same `RECORD_NOT_FOUND` / 404 the
11061106
// HTTP bridge's `callData` path throws for the identical miss.
11071107
if (!existing) throw recordNotFoundError(object, id);
1108-
await engine.update(object, data, { where: { id }, context });
1108+
// [#28844] An update the engine applied but whose parent roll-up it could
1109+
// not recompute (`ERR_SUMMARY_RECOMPUTE`) answers the update it is,
1110+
// attached to the refusal by #28781's carrier as `create` does; every
1111+
// other throw, a refusal before the write among them, propagates as is.
1112+
let stale: SummaryRecomputeRefusal | undefined;
1113+
try {
1114+
await engine.update(object, data, { where: { id }, context });
1115+
} catch (e) {
1116+
stale = staleSummaryRefusalOrThrow(e);
1117+
}
11091118
const record = { ...(existing as Record<string, unknown>), ...data };
11101119
// [#8497] The engine's update RESULT is deliberately discarded here (this
11111120
// arm echoes the read-path row plus the caller's own patch), so no STORED
@@ -1117,7 +1126,9 @@ export function createStdioDataBridge(deps: StdioDataBridgeDeps): McpDataBridge
11171126
// property literally true on every verb of this bridge rather than true-
11181127
// by-argument on one.
11191128
omitInternalFieldsFromWriteResponse(await readObjectDeclaration(reads, object), record);
1120-
return { object, id, record };
1129+
const answer = { object, id, record };
1130+
if (stale) throw withEarnedAnswer(stale, answer);
1131+
return answer;
11211132
},
11221133

11231134
async remove(object, id) {
@@ -1128,9 +1139,18 @@ export function createStdioDataBridge(deps: StdioDataBridgeDeps): McpDataBridge
11281139
const existing = await findById(engine, object, id, context);
11291140
// Same shared envelope as `update`, above.
11301141
if (!existing) throw recordNotFoundError(object, id);
1131-
await engine.delete(object, { where: { id }, context });
1142+
// [#28844] As `update`, above: a delete applied under a failing roll-up
1143+
// answers the delete it is.
1144+
let stale: SummaryRecomputeRefusal | undefined;
1145+
try {
1146+
await engine.delete(object, { where: { id }, context });
1147+
} catch (e) {
1148+
stale = staleSummaryRefusalOrThrow(e);
1149+
}
11321150
// `success`, not `deleted` — the spec's `DeleteDataResponse` key (#5581).
1133-
return { object, id, success: true };
1151+
const answer = { object, id, success: true };
1152+
if (stale) throw withEarnedAnswer(stale, answer);
1153+
return answer;
11341154
},
11351155
};
11361156

‎scripts/engine-double-contract.pinned.json‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@
8181
"verb": "update",
8282
"pinned": 1
8383
},
84+
{
85+
"file": "packages/mcp/src/mcp-update-delete-record-stale-summary.test.ts",
86+
"verb": "delete",
87+
"pinned": 1
88+
},
89+
{
90+
"file": "packages/mcp/src/mcp-update-delete-record-stale-summary.test.ts",
91+
"verb": "findOne",
92+
"pinned": 1
93+
},
94+
{
95+
"file": "packages/mcp/src/mcp-update-delete-record-stale-summary.test.ts",
96+
"verb": "update",
97+
"pinned": 1
98+
},
8499
{
85100
"file": "packages/mcp/src/mcp-write-response-internal-fields.tripwire.test.ts",
86101
"verb": "delete",

0 commit comments

Comments
 (0)