@wix/pathgrade 1.0.13 → 1.0.15
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.
- package/README.md +30 -0
- package/dist/adapters/jest/results.js +1 -1
- package/dist/adapters/node-test/index.js +1 -1
- package/dist/agents/claude/sdk-message-projector.js +3 -2
- package/dist/agents/claude/tool-permission-bridge.d.ts +4 -0
- package/dist/agents/claude/tool-permission-bridge.js +68 -1
- package/dist/agents/claude.d.ts +2 -0
- package/dist/agents/claude.js +55 -10
- package/dist/agents/codex-app-server/agent.js +94 -76
- package/dist/agents/codex-app-server/mcp-approval-correlator.d.ts +55 -0
- package/dist/agents/codex-app-server/mcp-approval-correlator.js +299 -0
- package/dist/core/canonical-json.d.ts +2 -0
- package/dist/core/canonical-json.js +51 -0
- package/dist/core/generated-mcp-protocol.d.ts +19 -0
- package/dist/core/generated-mcp-protocol.js +26 -0
- package/dist/core/mcp-mock.d.ts +1 -1
- package/dist/core/mcp-mock.js +24 -0
- package/dist/core/mcp-mock.types.d.ts +6 -0
- package/dist/core/mcp-schema-profile.d.ts +10 -0
- package/dist/core/mcp-schema-profile.js +91 -0
- package/dist/mcp-mock-server.js +29 -13
- package/dist/providers/mcp-config.js +4 -2
- package/dist/providers/sandbox.js +6 -0
- package/dist/providers/scripted-mcp-mock-host.d.ts +39 -0
- package/dist/providers/scripted-mcp-mock-host.js +368 -0
- package/dist/reporters/cli.js +4 -3
- package/dist/reporters/github-comment.js +1 -1
- package/dist/reporters/report-summary.js +1 -0
- package/dist/reporting/core.js +19 -5
- package/dist/reporting/types.d.ts +2 -1
- package/dist/runners/model-builders.js +1 -1
- package/dist/runners/model-validation.js +4 -1
- package/dist/runners/model.d.ts +1 -1
- package/dist/runners/report-projection.js +2 -2
- package/dist/runners/vitest-adapter.js +1 -1
- package/dist/sdk/agent.js +64 -19
- package/dist/sdk/diagnostics.d.ts +1 -0
- package/dist/sdk/diagnostics.js +6 -3
- package/dist/sdk/index.d.ts +4 -3
- package/dist/sdk/index.js +1 -1
- package/dist/sdk/lifecycle.js +3 -3
- package/dist/sdk/managed-session.d.ts +3 -0
- package/dist/sdk/managed-session.js +71 -26
- package/dist/sdk/mcp-event-input.d.ts +3 -0
- package/dist/sdk/mcp-event-input.js +8 -0
- package/dist/sdk/mcp-evidence.d.ts +39 -0
- package/dist/sdk/mcp-evidence.js +71 -12
- package/dist/sdk/mcp-mock-approvals.d.ts +40 -0
- package/dist/sdk/mcp-mock-approvals.js +235 -0
- package/dist/sdk/scripted-mcp-events.d.ts +24 -0
- package/dist/sdk/scripted-mcp-events.js +30 -0
- package/dist/sdk/types.d.ts +6 -2
- package/dist/tool-events.d.ts +16 -1
- package/dist/types.d.ts +3 -1
- package/dist/viewer.html +4 -4
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -47,6 +47,8 @@ By default, Pathgrade tries to reuse the agent CLI's native auth before falling
|
|
|
47
47
|
|
|
48
48
|
If you set `ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, or `CURSOR_API_BASE_URL`, set the matching API key too.
|
|
49
49
|
|
|
50
|
+
Do not add `copyFromHome: ['.codex']` just to reuse Codex authentication. When no OpenAI API key is available, Pathgrade automatically stages only `~/.codex/auth.json`. A deliberate full `.codex` copy remains supported, but it also imports host config, MCP definitions, plugins, caches, sessions, and other potentially large or sensitive state; Pathgrade prints a warning when requested.
|
|
51
|
+
|
|
50
52
|
Override credentials per test with `env`:
|
|
51
53
|
|
|
52
54
|
```typescript
|
|
@@ -332,6 +334,7 @@ const mock = mockMcpServer({
|
|
|
332
334
|
tools: [{
|
|
333
335
|
name: 'get_weather',
|
|
334
336
|
description: 'Get weather for a city',
|
|
337
|
+
annotations: { readOnlyHint: true },
|
|
335
338
|
when: 'weather',
|
|
336
339
|
response: { temp: 72, unit: 'F' },
|
|
337
340
|
}],
|
|
@@ -340,6 +343,33 @@ const mock = mockMcpServer({
|
|
|
340
343
|
const agent = await createAgent({ agent: 'claude', mcpMock: mock });
|
|
341
344
|
```
|
|
342
345
|
|
|
346
|
+
Read-only metadata is explicit: add `annotations.readOnlyHint: true` only when
|
|
347
|
+
the operation being modeled is read-only. Unannotated and explicitly mutating
|
|
348
|
+
mock tools retain the runtime's normal approval behavior.
|
|
349
|
+
|
|
350
|
+
To evaluate mutation approval behavior with Claude or Codex app-server, pass a
|
|
351
|
+
non-empty ordered `mcpMockApprovalRules` array. The first matching rule wins:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
const agent = await createAgent({
|
|
355
|
+
agent: 'codex',
|
|
356
|
+
transport: 'app-server',
|
|
357
|
+
mcpMock: mock,
|
|
358
|
+
mcpMockApprovalRules: [
|
|
359
|
+
{ serverName: 'sessions', toolName: 'remove_session',
|
|
360
|
+
argumentsContaining: { mode: 'delete' }, decision: 'deny' },
|
|
361
|
+
{ serverName: 'sessions', toolName: 'remove_session',
|
|
362
|
+
argumentsContaining: { mode: 'archive' }, decision: 'approve' },
|
|
363
|
+
],
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
Use `getMcpApproval()` for the simulated-user decision and
|
|
368
|
+
`wasMcpToolInvoked()` for receipt-backed execution (`true`, `false`, or
|
|
369
|
+
`undefined`). An `mcp_tool_call` event by itself proves only that an attempt was
|
|
370
|
+
observed. Mock responses are fixed fixtures; this mode does not model stateful
|
|
371
|
+
mutation or make later reads observe earlier writes.
|
|
372
|
+
|
|
343
373
|
## CLI
|
|
344
374
|
|
|
345
375
|
```bash
|
|
@@ -58,7 +58,7 @@ function runCase(input) {
|
|
|
58
58
|
const evaluations = input.evaluations.map((entry, index) => ({
|
|
59
59
|
id: `${attemptId}:evaluation-${index + 1}`,
|
|
60
60
|
attemptId,
|
|
61
|
-
score: entry.score,
|
|
61
|
+
...(entry.score !== undefined ? { score: entry.score } : {}),
|
|
62
62
|
...(entry.trial ? { trial: entry.trial } : {}),
|
|
63
63
|
...(entry.diagnostics ? { diagnostics: entry.diagnostics } : {}),
|
|
64
64
|
...(entry.resultKind ? { resultKind: entry.resultKind } : {}),
|
|
@@ -67,7 +67,7 @@ function toReportEvaluations(evaluations) {
|
|
|
67
67
|
if (evaluations.length === 0)
|
|
68
68
|
return undefined;
|
|
69
69
|
return evaluations.map(entry => ({
|
|
70
|
-
score: entry.score,
|
|
70
|
+
...(entry.score !== undefined ? { score: entry.score } : {}),
|
|
71
71
|
trial: entry.trial,
|
|
72
72
|
diagnostics: entry.diagnostics,
|
|
73
73
|
resultKind: entry.resultKind,
|
|
@@ -22,6 +22,7 @@ const SDK_ERROR_SUBTYPES = [
|
|
|
22
22
|
'error_max_structured_output_retries',
|
|
23
23
|
];
|
|
24
24
|
import { TOOL_NAME_MAP, buildSummary, enrichSkillEvents } from '../../tool-events.js';
|
|
25
|
+
import { attachOriginalMcpInput } from '../../sdk/mcp-event-input.js';
|
|
25
26
|
import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
|
|
26
27
|
import { applyObservedToolResult, extractObservedToolResults, } from './tool-results.js';
|
|
27
28
|
export function projectSdkMessages(input) {
|
|
@@ -202,7 +203,7 @@ function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMc
|
|
|
202
203
|
tool: mcpTool.tool,
|
|
203
204
|
status: 'incomplete',
|
|
204
205
|
};
|
|
205
|
-
return {
|
|
206
|
+
return attachOriginalMcpInput({
|
|
206
207
|
action: 'mcp_tool_call',
|
|
207
208
|
provider: 'claude',
|
|
208
209
|
providerToolName: normalizedProviderToolName,
|
|
@@ -214,7 +215,7 @@ function buildToolEvent(block, turnNumber, answerStore, mcpServerNames, deniedMc
|
|
|
214
215
|
summary: `MCP tool ${normalizedProviderToolName}`,
|
|
215
216
|
confidence: 'high',
|
|
216
217
|
rawSnippet: JSON.stringify(block).slice(0, 200),
|
|
217
|
-
};
|
|
218
|
+
}, rawInput ?? {});
|
|
218
219
|
}
|
|
219
220
|
const action = TOOL_NAME_MAP[providerToolName] ?? 'unknown';
|
|
220
221
|
const args = action === 'ask_user'
|
|
@@ -3,6 +3,8 @@ import type { AskBus } from '../../sdk/ask-bus/types.js';
|
|
|
3
3
|
import { type McpSafetyOptions } from '../../sdk/mcp-safety.js';
|
|
4
4
|
import type { AskUserAnswerStore } from './ask-user-answer-store.js';
|
|
5
5
|
import type { ClaudeDeniedMcpEventStore } from './denied-mcp-event-store.js';
|
|
6
|
+
import type { ScriptedMcpMockHost } from '../../providers/scripted-mcp-mock-host.js';
|
|
7
|
+
import type { ToolEvent } from '../../tool-events.js';
|
|
6
8
|
export interface ClaudeToolPermissionBridgeDeps {
|
|
7
9
|
askBus: AskBus;
|
|
8
10
|
getTurnNumber: () => number;
|
|
@@ -10,6 +12,8 @@ export interface ClaudeToolPermissionBridgeDeps {
|
|
|
10
12
|
mcpServerNames: readonly string[];
|
|
11
13
|
mcpSafety?: McpSafetyOptions;
|
|
12
14
|
deniedMcpEvents?: ClaudeDeniedMcpEventStore;
|
|
15
|
+
scriptedMcpHost?: ScriptedMcpMockHost;
|
|
16
|
+
recordScriptedEvent?: (event: ToolEvent) => void;
|
|
13
17
|
}
|
|
14
18
|
export type ClaudeToolPermissionBridge = CanUseTool & {
|
|
15
19
|
lastError(): Error | null;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { decideMcpToolCall, redactMcpSecrets, } from '../../sdk/mcp-safety.js';
|
|
2
2
|
import { createAskUserBridge, } from './ask-user-bridge.js';
|
|
3
3
|
import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
|
|
4
|
+
import { canonicalizeJson } from '../../core/canonical-json.js';
|
|
5
|
+
import { buildScriptedMcpApprovalEvent, buildScriptedMcpDeniedCallEvent, } from '../../sdk/scripted-mcp-events.js';
|
|
4
6
|
export function createClaudeToolPermissionBridge(deps) {
|
|
5
7
|
const askUserBridge = createAskUserBridge({
|
|
6
8
|
askBus: deps.askBus,
|
|
7
9
|
getTurnNumber: deps.getTurnNumber,
|
|
8
10
|
answerStore: deps.answerStore,
|
|
9
11
|
});
|
|
12
|
+
const scriptedMemo = new Map();
|
|
10
13
|
const canUseTool = async (toolName, input, options) => {
|
|
11
14
|
if (toolName === 'AskUserQuestion') {
|
|
12
15
|
return askUserBridge(toolName, input, options);
|
|
@@ -16,6 +19,56 @@ export function createClaudeToolPermissionBridge(deps) {
|
|
|
16
19
|
if (parsed.kind === 'non_mcp') {
|
|
17
20
|
return allow(input);
|
|
18
21
|
}
|
|
22
|
+
if (runMode === 'mock' && deps.scriptedMcpHost && parsed.kind === 'mcp') {
|
|
23
|
+
const args = asRecord(input);
|
|
24
|
+
const host = deps.scriptedMcpHost;
|
|
25
|
+
if (host.isReadOnly(parsed.server, parsed.tool))
|
|
26
|
+
return allow(input);
|
|
27
|
+
const memoKey = `${deps.getTurnNumber()}\0${options.toolUseID}`;
|
|
28
|
+
let fingerprint;
|
|
29
|
+
try {
|
|
30
|
+
fingerprint = canonicalizeJson({ toolName, input: args });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
host.failProtocol('invalid Claude MCP approval authority fields');
|
|
34
|
+
recordClaudeProtocolFailure(deps, options.toolUseID, parsed.server, parsed.tool, args, 'malformed_request');
|
|
35
|
+
return { behavior: 'deny', message: 'The MCP approval request failed validation.' };
|
|
36
|
+
}
|
|
37
|
+
const prior = scriptedMemo.get(memoKey);
|
|
38
|
+
if (prior) {
|
|
39
|
+
if (prior.fingerprint === fingerprint)
|
|
40
|
+
return prior.result;
|
|
41
|
+
host.failProtocol('conflicting duplicate Claude MCP approval callback');
|
|
42
|
+
recordClaudeProtocolFailure(deps, options.toolUseID, parsed.server, parsed.tool, args);
|
|
43
|
+
return { behavior: 'deny', message: 'The MCP approval request failed validation.' };
|
|
44
|
+
}
|
|
45
|
+
const result = Promise.resolve().then(() => {
|
|
46
|
+
const decision = host.decide(parsed.server, parsed.tool, args);
|
|
47
|
+
deps.recordScriptedEvent?.(buildScriptedMcpApprovalEvent({
|
|
48
|
+
provider: 'claude', toolUseId: options.toolUseID, turnNumber: deps.getTurnNumber(),
|
|
49
|
+
serverName: parsed.server, toolName: parsed.tool, decision,
|
|
50
|
+
}));
|
|
51
|
+
if (decision.decision === 'approve')
|
|
52
|
+
return allow(input);
|
|
53
|
+
const outcome = decision.outcome === 'matched' ? 'user_denied' : 'protocol_error';
|
|
54
|
+
deps.deniedMcpEvents?.record(options.toolUseID, buildScriptedMcpDeniedCallEvent({
|
|
55
|
+
provider: 'claude', toolUseId: options.toolUseID, turnNumber: deps.getTurnNumber(),
|
|
56
|
+
serverName: parsed.server, toolName: parsed.tool, args, outcome,
|
|
57
|
+
}));
|
|
58
|
+
return { behavior: 'deny', message: decision.outcome === 'matched'
|
|
59
|
+
? 'The user denied this MCP tool call.'
|
|
60
|
+
: 'The MCP approval request failed validation.' };
|
|
61
|
+
});
|
|
62
|
+
scriptedMemo.set(memoKey, { fingerprint, result });
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
if (runMode === 'mock' && deps.scriptedMcpHost && parsed.kind === 'unrecognized') {
|
|
66
|
+
deps.scriptedMcpHost.failProtocol(`unrecognized Claude SDK MCP tool name: ${toolName}`);
|
|
67
|
+
return {
|
|
68
|
+
behavior: 'deny',
|
|
69
|
+
message: `unrecognized Claude SDK MCP tool name: ${toolName}`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
19
72
|
if (runMode === 'mock') {
|
|
20
73
|
return allow(input);
|
|
21
74
|
}
|
|
@@ -44,9 +97,23 @@ export function createClaudeToolPermissionBridge(deps) {
|
|
|
44
97
|
};
|
|
45
98
|
const bridge = canUseTool;
|
|
46
99
|
bridge.lastError = () => askUserBridge.lastError();
|
|
47
|
-
bridge.clearLastError = () =>
|
|
100
|
+
bridge.clearLastError = () => {
|
|
101
|
+
askUserBridge.clearLastError();
|
|
102
|
+
scriptedMemo.clear();
|
|
103
|
+
};
|
|
48
104
|
return bridge;
|
|
49
105
|
}
|
|
106
|
+
function recordClaudeProtocolFailure(deps, toolUseId, serverName, toolName, args, reason = 'conflicting_duplicate') {
|
|
107
|
+
deps.recordScriptedEvent?.(buildScriptedMcpApprovalEvent({
|
|
108
|
+
provider: 'claude', toolUseId, turnNumber: deps.getTurnNumber(), serverName, toolName,
|
|
109
|
+
decision: { decision: 'deny', outcome: 'protocol_error', reason },
|
|
110
|
+
status: 'error',
|
|
111
|
+
}));
|
|
112
|
+
deps.deniedMcpEvents?.record(toolUseId, buildScriptedMcpDeniedCallEvent({
|
|
113
|
+
provider: 'claude', toolUseId, turnNumber: deps.getTurnNumber(),
|
|
114
|
+
serverName, toolName, args, outcome: 'protocol_error',
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
50
117
|
function allow(input) {
|
|
51
118
|
return { behavior: 'allow', updatedInput: asRecord(input) };
|
|
52
119
|
}
|
package/dist/agents/claude.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface ClaudeAgentDeps {
|
|
|
43
43
|
now?: () => Date;
|
|
44
44
|
/** Monotonic clock seam for deterministic tool duration tests. */
|
|
45
45
|
monotonicNow?: () => number;
|
|
46
|
+
/** Grace period before force-closing an unresponsive SDK query. */
|
|
47
|
+
queryAbortGraceMs?: number;
|
|
46
48
|
}
|
|
47
49
|
export interface ClaudeAgentOptions {
|
|
48
50
|
/**
|
package/dist/agents/claude.js
CHANGED
|
@@ -47,6 +47,19 @@ function createLinkedAbortController(signal) {
|
|
|
47
47
|
function getTurnAbortSignal(sessionOptions) {
|
|
48
48
|
return sessionOptions?.getAbortSignal?.() ?? sessionOptions?.abortSignal;
|
|
49
49
|
}
|
|
50
|
+
async function settlesWithin(promise, timeoutMs) {
|
|
51
|
+
let timer;
|
|
52
|
+
try {
|
|
53
|
+
return await Promise.race([
|
|
54
|
+
promise.then(() => true),
|
|
55
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(false), timeoutMs); }),
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
finally {
|
|
59
|
+
if (timer)
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
50
63
|
export class ClaudeAgent extends BaseAgent {
|
|
51
64
|
deps;
|
|
52
65
|
opts;
|
|
@@ -65,6 +78,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
65
78
|
const platform = this.deps.platform ?? process.platform;
|
|
66
79
|
const hostEnv = this.deps.hostEnv ?? process.env;
|
|
67
80
|
const envExecutable = this.deps.envExecutable ?? process.env.PATHGRADE_CLAUDE_CODE_EXECUTABLE;
|
|
81
|
+
const queryAbortGraceMs = Math.max(0, this.deps.queryAbortGraceMs ?? 250);
|
|
68
82
|
const runtimeEnv = getRuntimeEnv(runtime);
|
|
69
83
|
const sensitiveValues = collectSensitiveEnvValues(runtimeEnv);
|
|
70
84
|
const sandboxedSpawn = createSandboxedClaudeSpawn({
|
|
@@ -86,7 +100,8 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
86
100
|
mcpSafety: sessionOptions?.mcpSafety,
|
|
87
101
|
});
|
|
88
102
|
await assertStdioMcpServersStartForClaudeSdk(mcpMountOptions);
|
|
89
|
-
const mcpServers =
|
|
103
|
+
const mcpServers = sessionOptions?.scriptedMcpHost?.claudeServers
|
|
104
|
+
?? await mountMcpForClaudeSdk(mcpMountOptions);
|
|
90
105
|
let priorSessionId;
|
|
91
106
|
let turnNumber = 0;
|
|
92
107
|
// The live ask-user bridge resolves AskUserQuestion through the bus
|
|
@@ -97,12 +112,19 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
97
112
|
// collisions.
|
|
98
113
|
let answerStore = createAskUserAnswerStore();
|
|
99
114
|
let deniedMcpEvents = createClaudeDeniedMcpEventStore();
|
|
115
|
+
let scriptedEvents = [];
|
|
116
|
+
let activeQuery;
|
|
117
|
+
let activeController;
|
|
118
|
+
let quiescence = Promise.resolve();
|
|
119
|
+
let resolveQuiescence;
|
|
100
120
|
const bridge = createClaudeToolPermissionBridge({
|
|
101
121
|
askBus,
|
|
102
122
|
getTurnNumber: () => turnNumber,
|
|
103
123
|
answerStore: { record: (id, e) => answerStore.record(id, e), get: (id) => answerStore.get(id) },
|
|
104
124
|
mcpServerNames: mcpServers ? Object.keys(mcpServers) : [],
|
|
105
125
|
mcpSafety: sessionOptions?.mcpSafety,
|
|
126
|
+
scriptedMcpHost: sessionOptions?.scriptedMcpHost,
|
|
127
|
+
recordScriptedEvent: (event) => scriptedEvents.push(event),
|
|
106
128
|
deniedMcpEvents: {
|
|
107
129
|
record: (id, event) => deniedMcpEvents.record(id, event),
|
|
108
130
|
get: (id) => deniedMcpEvents.get(id),
|
|
@@ -114,9 +136,11 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
114
136
|
turnNumber += 1;
|
|
115
137
|
answerStore = createAskUserAnswerStore();
|
|
116
138
|
deniedMcpEvents = createClaudeDeniedMcpEventStore();
|
|
139
|
+
scriptedEvents = [];
|
|
117
140
|
// Clear any ask-bus rejection captured on a prior turn so a
|
|
118
141
|
// stale error never causes a spurious result on this turn.
|
|
119
142
|
bridge.clearLastError();
|
|
143
|
+
activeController = createLinkedAbortController(getTurnAbortSignal(sessionOptions));
|
|
120
144
|
const sdkOptions = buildClaudeSdkOptions({
|
|
121
145
|
workspacePath,
|
|
122
146
|
spawnClaudeCodeProcess: sandboxedSpawn,
|
|
@@ -126,21 +150,31 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
126
150
|
claudeCodeExecutable,
|
|
127
151
|
resume: priorSessionId,
|
|
128
152
|
mcpServers,
|
|
129
|
-
abortController:
|
|
153
|
+
abortController: activeController,
|
|
130
154
|
});
|
|
131
155
|
const messages = [];
|
|
132
156
|
const messageTimings = [];
|
|
133
157
|
const now = this.deps.now ?? (() => new Date());
|
|
134
158
|
const monotonicNow = this.deps.monotonicNow ?? (() => performance.now());
|
|
135
159
|
const stream = queryFn({ prompt: message, options: sdkOptions });
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
160
|
+
activeQuery = stream;
|
|
161
|
+
quiescence = new Promise((resolve) => { resolveQuiescence = resolve; });
|
|
162
|
+
try {
|
|
163
|
+
for await (const msg of stream) {
|
|
164
|
+
messages.push(msg);
|
|
165
|
+
messageTimings.push(hasToolLifecycleBoundary(msg)
|
|
166
|
+
? {
|
|
167
|
+
receivedAt: now().toISOString(),
|
|
168
|
+
receivedMonotonicMs: monotonicNow(),
|
|
169
|
+
}
|
|
170
|
+
: undefined);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
activeQuery = undefined;
|
|
175
|
+
activeController = undefined;
|
|
176
|
+
resolveQuiescence?.();
|
|
177
|
+
resolveQuiescence = undefined;
|
|
144
178
|
}
|
|
145
179
|
// The legacy NDJSON parser only synthesized the slash-command
|
|
146
180
|
// `use_skill` event from the *opening* user message. The Claude
|
|
@@ -161,6 +195,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
161
195
|
deniedMcpEvents,
|
|
162
196
|
sensitiveValues,
|
|
163
197
|
});
|
|
198
|
+
projected.result.toolEvents = [...scriptedEvents, ...projected.result.toolEvents];
|
|
164
199
|
// Capture the SDK-reported session id BEFORE checking for a bus
|
|
165
200
|
// rejection so the next turn's `Options.resume` points at this
|
|
166
201
|
// turn's session even when the turn ended in an ask-bus error.
|
|
@@ -191,6 +226,16 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
191
226
|
return {
|
|
192
227
|
start: ({ message }) => runTurn(message),
|
|
193
228
|
reply: ({ message }) => runTurn(message),
|
|
229
|
+
async dispose() {
|
|
230
|
+
const query = activeQuery;
|
|
231
|
+
activeController?.abort();
|
|
232
|
+
if (!query)
|
|
233
|
+
return;
|
|
234
|
+
if (await settlesWithin(quiescence, queryAbortGraceMs))
|
|
235
|
+
return;
|
|
236
|
+
query.close();
|
|
237
|
+
await settlesWithin(quiescence, queryAbortGraceMs);
|
|
238
|
+
},
|
|
194
239
|
};
|
|
195
240
|
}
|
|
196
241
|
}
|