@saccolabs/pi-claude-cli 0.4.12 → 0.4.13
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 +3 -0
- package/index.ts +30 -0
- package/package.json +1 -1
- package/src/event-bridge.ts +8 -0
- package/src/provider.ts +28 -0
- package/src/task-tracker.ts +191 -0
- package/src/types.ts +64 -0
package/README.md
CHANGED
|
@@ -48,6 +48,9 @@ Requires the `claude` binary on your login-shell PATH (`npm install -g @anthropi
|
|
|
48
48
|
- Native tool execution: the CLI runs its own tools; guards are injected as Claude Code PreToolUse hooks via `PI_CLAUDE_CLI_SETTINGS`
|
|
49
49
|
- Reports account rate-limit state (window, reset, overage) to the front-end
|
|
50
50
|
on the `claude-rate-limit` status key — never mixed into turn content
|
|
51
|
+
- Surfaces sub-agent fan-outs: one marker when a `Task` agent starts and one
|
|
52
|
+
when it reports, plus live per-agent progress on the `claude-subagents`
|
|
53
|
+
status key — so a fan-out is no longer a blank pane
|
|
51
54
|
- Configurable thinking effort across the full ladder (low to max), mapped 1:1 for every model: the level the host asks for is the level the CLI gets
|
|
52
55
|
- Cross-platform subprocess management (Windows, macOS, Linux)
|
|
53
56
|
- Inactivity timeout and process registry for cleanup
|
package/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import { getCustomToolDefs, writeMcpConfig } from "./src/mcp-config.js";
|
|
18
18
|
import { rewriteOverflowMessage } from "./src/overflow.js";
|
|
19
19
|
import { buildRateLimitPayload, rateLimitIdentity } from "./src/rate-limit.js";
|
|
20
|
+
import type { TaskTrackerState } from "./src/types.js";
|
|
20
21
|
|
|
21
22
|
// Kill all active Claude subprocesses on process exit to prevent orphans
|
|
22
23
|
process.on("exit", killAllProcesses);
|
|
@@ -58,6 +59,33 @@ function publishRateLimit(info: Record<string, unknown>): void {
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Live sub-agent state, on its own status key.
|
|
64
|
+
*
|
|
65
|
+
* Same reasoning as the rate-limit channel: this is state ABOUT the turn, not
|
|
66
|
+
* content OF it. `task_progress` fires once per sub-agent tool call (roughly
|
|
67
|
+
* 700 times in the incident that motivated #23), so folding it into the
|
|
68
|
+
* transcript would bury the turn and cost context on every later replay. The
|
|
69
|
+
* durable half — one marker when a sub-agent starts, one when it finishes —
|
|
70
|
+
* goes in the turn instead, and needs no host change to render.
|
|
71
|
+
*/
|
|
72
|
+
const SUBAGENTS_STATUS_KEY = "claude-subagents";
|
|
73
|
+
/** Last payload pushed, so an unchanged snapshot does not rewrite the status. */
|
|
74
|
+
let lastSubagentsJson: string | undefined;
|
|
75
|
+
|
|
76
|
+
function publishTaskProgress(state: TaskTrackerState): void {
|
|
77
|
+
const setStatus = uiContext?.ui?.setStatus;
|
|
78
|
+
if (typeof setStatus !== "function") return;
|
|
79
|
+
const json = JSON.stringify(state);
|
|
80
|
+
if (json === lastSubagentsJson) return;
|
|
81
|
+
lastSubagentsJson = json;
|
|
82
|
+
try {
|
|
83
|
+
setStatus.call(uiContext!.ui, SUBAGENTS_STATUS_KEY, json);
|
|
84
|
+
} catch {
|
|
85
|
+
/* never break a turn over a status push */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
61
89
|
let mcpConfigPath: string | undefined;
|
|
62
90
|
let mcpConfigResolved = false;
|
|
63
91
|
|
|
@@ -130,6 +158,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
130
158
|
pi.on("session_start", async (_event: unknown, ctx: unknown) => {
|
|
131
159
|
uiContext = ctx as typeof uiContext;
|
|
132
160
|
lastRateLimitJson = undefined;
|
|
161
|
+
lastSubagentsJson = undefined;
|
|
133
162
|
const allTools = pi.getAllTools();
|
|
134
163
|
if (Array.isArray(allTools)) {
|
|
135
164
|
pi.setActiveTools(allTools.map((t: any) => t.name));
|
|
@@ -146,6 +175,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
146
175
|
...options,
|
|
147
176
|
mcpConfigPath: configPath,
|
|
148
177
|
onRateLimit: publishRateLimit,
|
|
178
|
+
onTaskProgress: publishTaskProgress,
|
|
149
179
|
});
|
|
150
180
|
};
|
|
151
181
|
|
package/package.json
CHANGED
package/src/event-bridge.ts
CHANGED
|
@@ -56,6 +56,13 @@ export interface EventBridge {
|
|
|
56
56
|
* otherwise be invisible — each becomes a one-line marker text block.
|
|
57
57
|
*/
|
|
58
58
|
handleAssistantEnvelope(envelope: ClaudeAssistantEnvelope): void;
|
|
59
|
+
/**
|
|
60
|
+
* Append a pre-built marker text block. Used for sub-agent lifecycle, whose
|
|
61
|
+
* events arrive as top-level `system` envelopes rather than content blocks
|
|
62
|
+
* (`src/task-tracker.ts`). The bridge stays the only writer of
|
|
63
|
+
* `output.content`.
|
|
64
|
+
*/
|
|
65
|
+
appendMarker(text: string): void;
|
|
59
66
|
/**
|
|
60
67
|
* The final `result` envelope: authoritative cumulative usage for the
|
|
61
68
|
* whole episode, plus a safety net that appends the final answer text if
|
|
@@ -649,6 +656,7 @@ export function createEventBridge(
|
|
|
649
656
|
return {
|
|
650
657
|
handleEvent,
|
|
651
658
|
handleAssistantEnvelope,
|
|
659
|
+
appendMarker: appendTextBlock,
|
|
652
660
|
applyResult,
|
|
653
661
|
getOutput: () => output,
|
|
654
662
|
};
|
package/src/provider.ts
CHANGED
|
@@ -40,6 +40,8 @@ import {
|
|
|
40
40
|
} from "./process-manager.js";
|
|
41
41
|
import { parseLine } from "./stream-parser.js";
|
|
42
42
|
import { createEventBridge } from "./event-bridge.js";
|
|
43
|
+
import { createTaskTracker, isTaskSubtype } from "./task-tracker.js";
|
|
44
|
+
import type { TaskTrackerState } from "./types.js";
|
|
43
45
|
import { handleControlRequest } from "./control-handler.js";
|
|
44
46
|
import { mapThinkingEffort } from "./thinking-config.js";
|
|
45
47
|
import { isHandoffClaudeTool } from "./tool-mapping.js";
|
|
@@ -66,6 +68,12 @@ type StreamViaCLiOptions = SimpleStreamOptions & {
|
|
|
66
68
|
mcpConfigPath?: string;
|
|
67
69
|
/** Called with account rate-limit state as the CLI reports it. */
|
|
68
70
|
onRateLimit?: (info: Record<string, unknown>) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Called with live sub-agent state as the CLI reports it. Ephemeral: this is
|
|
73
|
+
* progress, not transcript, and must never be folded into turn content. The
|
|
74
|
+
* durable half (start/finish) rides in the turn as markers instead.
|
|
75
|
+
*/
|
|
76
|
+
onTaskProgress?: (state: TaskTrackerState) => void;
|
|
69
77
|
};
|
|
70
78
|
|
|
71
79
|
/**
|
|
@@ -193,6 +201,9 @@ export function streamViaCli(
|
|
|
193
201
|
|
|
194
202
|
// Create event bridge (before endStreamWithError so bridge is in scope)
|
|
195
203
|
const bridge = createEventBridge(stream, model);
|
|
204
|
+
// Per-attempt: a resume-miss retry replays the episode, and its
|
|
205
|
+
// sub-agents must not be counted twice.
|
|
206
|
+
const taskTracker = createTaskTracker();
|
|
196
207
|
|
|
197
208
|
// Guard against double stream.end() and double error events.
|
|
198
209
|
// First error path wins; subsequent ones are no-ops.
|
|
@@ -352,6 +363,23 @@ export function streamViaCli(
|
|
|
352
363
|
/* a status push must never break a turn */
|
|
353
364
|
}
|
|
354
365
|
}
|
|
366
|
+
} else if (
|
|
367
|
+
msg.type === "system" &&
|
|
368
|
+
isTaskSubtype((msg as any).subtype)
|
|
369
|
+
) {
|
|
370
|
+
// Sub-agent lifecycle. The agents' own envelopes carry
|
|
371
|
+
// parent_tool_use_id and stay internal to the CLI, but these arrive
|
|
372
|
+
// at top level even for deeply nested agents — the one channel that
|
|
373
|
+
// makes a fan-out visible at all (#23).
|
|
374
|
+
if (!selfInterrupted) {
|
|
375
|
+
const marker = taskTracker.apply(msg as any);
|
|
376
|
+
if (marker) bridge.appendMarker(marker);
|
|
377
|
+
try {
|
|
378
|
+
options?.onTaskProgress?.(taskTracker.snapshot());
|
|
379
|
+
} catch {
|
|
380
|
+
/* a status push must never break a turn */
|
|
381
|
+
}
|
|
382
|
+
}
|
|
355
383
|
} else if (msg.type === "assistant") {
|
|
356
384
|
// Complete-block envelopes: marker text for the CLI's own tool
|
|
357
385
|
// executions (built-ins, WebSearch, user MCP, …), which in observer
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent (Task) visibility.
|
|
3
|
+
*
|
|
4
|
+
* The CLI runs sub-agents inside its own process. Their stream envelopes carry
|
|
5
|
+
* `parent_tool_use_id` and are deliberately not forwarded — they are the CLI's
|
|
6
|
+
* internal loop, and forwarding hundreds of nested tool calls would bury the
|
|
7
|
+
* turn. But dropping them left a host with nothing at all: a single
|
|
8
|
+
* `[Claude Code · Task {…}]` marker and then silence for as long as the
|
|
9
|
+
* fan-out ran. One pidex turn sat blank for eight minutes behind 14 nested
|
|
10
|
+
* agents and was killed as hung (issue #23).
|
|
11
|
+
*
|
|
12
|
+
* The CLI already publishes a purpose-built feed for exactly this, and the
|
|
13
|
+
* provider was not reading it. `system` envelopes with a `task_*` subtype
|
|
14
|
+
* arrive at TOP level (`parent_tool_use_id` is null) even for agents nested
|
|
15
|
+
* several deep, verified on claude 2.1.231 at spawn depth 2. They are low
|
|
16
|
+
* volume and carry description, sub-agent type, tool count, tokens and
|
|
17
|
+
* duration.
|
|
18
|
+
*
|
|
19
|
+
* Two channels, split by durability:
|
|
20
|
+
*
|
|
21
|
+
* - **Lifecycle goes in the turn** as marker text. `task_started` and the
|
|
22
|
+
* terminal `task_notification` are durable facts about what the turn did,
|
|
23
|
+
* and belong in the transcript beside the CLI's other tool markers. Two
|
|
24
|
+
* lines per sub-agent, so a 14-agent fan-out costs 28.
|
|
25
|
+
* - **Progress goes out of band**, like `rate_limit_event` before it.
|
|
26
|
+
* `task_progress` fires once per sub-agent tool call — ~700 times in the
|
|
27
|
+
* incident above — which is live state, not transcript. It must never be
|
|
28
|
+
* folded into turn content.
|
|
29
|
+
*
|
|
30
|
+
* What this deliberately does NOT do: build a tree. No task envelope names its
|
|
31
|
+
* parent task, so a nested agent is indistinguishable from a top-level one
|
|
32
|
+
* here. The list is flat, and honestly so.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type {
|
|
36
|
+
ClaudeTaskEvent,
|
|
37
|
+
TaskSnapshot,
|
|
38
|
+
TaskTrackerState,
|
|
39
|
+
} from "./types.js";
|
|
40
|
+
|
|
41
|
+
/** Marker argument previews are truncated to keep one row on one line. */
|
|
42
|
+
const ARGS_PREVIEW_LIMIT = 120;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a `[Claude Code · Task …]` marker.
|
|
46
|
+
*
|
|
47
|
+
* WIRE CONTRACT — the same shape `event-bridge.ts` emits for CLI-side tool
|
|
48
|
+
* executions, and front-ends parse it with
|
|
49
|
+
* `/^\[Claude Code · ([^\s\]]+)(?:\s+([\s\S]*))?\]$/`. The tool name must stay
|
|
50
|
+
* space-free; everything else rides in the argument JSON, which is truncated
|
|
51
|
+
* here and must never be parsed as JSON by a consumer.
|
|
52
|
+
*/
|
|
53
|
+
function taskMarker(args: Record<string, unknown>): string {
|
|
54
|
+
let preview = "";
|
|
55
|
+
try {
|
|
56
|
+
const json = JSON.stringify(args);
|
|
57
|
+
preview =
|
|
58
|
+
json === "{}"
|
|
59
|
+
? ""
|
|
60
|
+
: ` ${json.slice(0, ARGS_PREVIEW_LIMIT)}${json.length > ARGS_PREVIEW_LIMIT ? "…" : ""}`;
|
|
61
|
+
} catch {
|
|
62
|
+
/* unserializable — the marker still names the tool */
|
|
63
|
+
}
|
|
64
|
+
return `[Claude Code · Task${preview}]`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface TaskTracker {
|
|
68
|
+
/**
|
|
69
|
+
* Fold one `task_*` envelope in. Returns a marker string when the event is a
|
|
70
|
+
* durable lifecycle transition (start, finish) and nothing when it is live
|
|
71
|
+
* progress. Unknown subtypes and events without a `task_id` are ignored.
|
|
72
|
+
*/
|
|
73
|
+
apply(event: ClaudeTaskEvent): string | undefined;
|
|
74
|
+
/** Current state of every sub-agent seen this episode. */
|
|
75
|
+
snapshot(): TaskTrackerState;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createTaskTracker(): TaskTracker {
|
|
79
|
+
/** Insertion-ordered, so the snapshot reads in launch order. */
|
|
80
|
+
const tasks = new Map<string, TaskSnapshot>();
|
|
81
|
+
|
|
82
|
+
function upsert(id: string, patch: Partial<TaskSnapshot>): TaskSnapshot {
|
|
83
|
+
const existing = tasks.get(id);
|
|
84
|
+
const next: TaskSnapshot = existing ?? {
|
|
85
|
+
taskId: id,
|
|
86
|
+
// A progress event can arrive before the start it belongs to if the CLI
|
|
87
|
+
// reorders; the id is a truthful placeholder until the start names it.
|
|
88
|
+
description: id,
|
|
89
|
+
status: "running",
|
|
90
|
+
};
|
|
91
|
+
Object.assign(next, patch);
|
|
92
|
+
tasks.set(id, next);
|
|
93
|
+
return next;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
apply(event: ClaudeTaskEvent): string | undefined {
|
|
98
|
+
const id = event.task_id;
|
|
99
|
+
if (!id) return undefined;
|
|
100
|
+
|
|
101
|
+
switch (event.subtype) {
|
|
102
|
+
case "task_started": {
|
|
103
|
+
const task = upsert(id, {
|
|
104
|
+
description: event.description ?? id,
|
|
105
|
+
subagentType: event.subagent_type,
|
|
106
|
+
status: "running",
|
|
107
|
+
});
|
|
108
|
+
return taskMarker({
|
|
109
|
+
status: "started",
|
|
110
|
+
description: task.description,
|
|
111
|
+
subagent_type: task.subagentType,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
case "task_progress": {
|
|
116
|
+
upsert(id, {
|
|
117
|
+
// `description` on a progress event is the CURRENT step ("Running
|
|
118
|
+
// …"), not the task's own description. Keep them apart: the task
|
|
119
|
+
// name was set at start and must not be overwritten by a step.
|
|
120
|
+
currentStep: event.description,
|
|
121
|
+
subagentType: event.subagent_type ?? tasks.get(id)?.subagentType,
|
|
122
|
+
lastToolName: event.last_tool_name,
|
|
123
|
+
toolUses: event.usage?.tool_uses,
|
|
124
|
+
totalTokens: event.usage?.total_tokens,
|
|
125
|
+
durationMs: event.usage?.duration_ms,
|
|
126
|
+
});
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case "task_updated": {
|
|
131
|
+
const status = event.patch?.status;
|
|
132
|
+
upsert(id, status ? { status } : {});
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
case "task_notification": {
|
|
137
|
+
const task = upsert(id, {
|
|
138
|
+
status: event.status ?? "completed",
|
|
139
|
+
outputFile: event.output_file,
|
|
140
|
+
toolUses: event.usage?.tool_uses ?? tasks.get(id)?.toolUses,
|
|
141
|
+
totalTokens:
|
|
142
|
+
event.usage?.total_tokens ?? tasks.get(id)?.totalTokens,
|
|
143
|
+
durationMs: event.usage?.duration_ms ?? tasks.get(id)?.durationMs,
|
|
144
|
+
currentStep: undefined,
|
|
145
|
+
});
|
|
146
|
+
// The sub-agent's full report reaches the model as the Task tool's
|
|
147
|
+
// own result. Repeating it here would duplicate kilobytes into the
|
|
148
|
+
// transcript, so the marker carries the shape of the work, not its
|
|
149
|
+
// output.
|
|
150
|
+
return taskMarker({
|
|
151
|
+
status: task.status,
|
|
152
|
+
description: task.description,
|
|
153
|
+
tool_uses: task.toolUses,
|
|
154
|
+
total_tokens: task.totalTokens,
|
|
155
|
+
duration_ms: task.durationMs,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
default:
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
snapshot(): TaskTrackerState {
|
|
165
|
+
const list = [...tasks.values()].map((t) => ({ ...t }));
|
|
166
|
+
return {
|
|
167
|
+
tasks: list,
|
|
168
|
+
active: list.filter((t) => t.status === "running").length,
|
|
169
|
+
completed: list.filter((t) => t.status !== "running").length,
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The `system` subtypes this module handles. */
|
|
176
|
+
const TASK_SUBTYPES = new Set([
|
|
177
|
+
"task_started",
|
|
178
|
+
"task_progress",
|
|
179
|
+
"task_updated",
|
|
180
|
+
"task_notification",
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Whether a `system` envelope's subtype is sub-agent lifecycle.
|
|
185
|
+
*
|
|
186
|
+
* `system` also carries init/status/summary envelopes that have nothing to do
|
|
187
|
+
* with sub-agents, so the provider narrows before handing anything over.
|
|
188
|
+
*/
|
|
189
|
+
export function isTaskSubtype(subtype: unknown): boolean {
|
|
190
|
+
return typeof subtype === "string" && TASK_SUBTYPES.has(subtype);
|
|
191
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -106,6 +106,69 @@ export interface ClaudeSystemMessage {
|
|
|
106
106
|
tools?: unknown[];
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Sub-agent lifecycle, emitted by the CLI as `system` envelopes.
|
|
111
|
+
*
|
|
112
|
+
* These arrive at TOP level — `parent_tool_use_id` is null — even for agents
|
|
113
|
+
* nested several deep, verified on claude 2.1.231 at spawn depth 2. That is
|
|
114
|
+
* what makes them usable: the sub-agents' own `assistant` envelopes are
|
|
115
|
+
* tagged with `parent_tool_use_id` and stay internal to the CLI, but their
|
|
116
|
+
* lifecycle is published here in the open.
|
|
117
|
+
*
|
|
118
|
+
* `description` means two different things by subtype. On `task_started` it
|
|
119
|
+
* names the task; on `task_progress` it is the step running right now
|
|
120
|
+
* ("Running …"). `src/task-tracker.ts` keeps them apart.
|
|
121
|
+
*/
|
|
122
|
+
export interface ClaudeTaskEvent {
|
|
123
|
+
type: "system";
|
|
124
|
+
subtype:
|
|
125
|
+
| "task_started"
|
|
126
|
+
| "task_progress"
|
|
127
|
+
| "task_updated"
|
|
128
|
+
| "task_notification"
|
|
129
|
+
| string;
|
|
130
|
+
task_id?: string;
|
|
131
|
+
tool_use_id?: string;
|
|
132
|
+
description?: string;
|
|
133
|
+
subagent_type?: string;
|
|
134
|
+
task_type?: string;
|
|
135
|
+
/** Terminal status on `task_notification`. */
|
|
136
|
+
status?: string;
|
|
137
|
+
/** Where the CLI wrote the sub-agent's full report. */
|
|
138
|
+
output_file?: string;
|
|
139
|
+
/** Partial state change on `task_updated`. */
|
|
140
|
+
patch?: { status?: string; end_time?: number };
|
|
141
|
+
last_tool_name?: string;
|
|
142
|
+
usage?: {
|
|
143
|
+
total_tokens?: number;
|
|
144
|
+
tool_uses?: number;
|
|
145
|
+
duration_ms?: number;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** One sub-agent's state, as the host sees it. */
|
|
150
|
+
export interface TaskSnapshot {
|
|
151
|
+
taskId: string;
|
|
152
|
+
/** Names the task. Set at `task_started`, never overwritten by a step. */
|
|
153
|
+
description: string;
|
|
154
|
+
subagentType?: string;
|
|
155
|
+
status: string;
|
|
156
|
+
/** The step running right now, cleared when the task ends. */
|
|
157
|
+
currentStep?: string;
|
|
158
|
+
lastToolName?: string;
|
|
159
|
+
toolUses?: number;
|
|
160
|
+
totalTokens?: number;
|
|
161
|
+
durationMs?: number;
|
|
162
|
+
outputFile?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Every sub-agent seen this episode, in launch order. */
|
|
166
|
+
export interface TaskTrackerState {
|
|
167
|
+
tasks: TaskSnapshot[];
|
|
168
|
+
active: number;
|
|
169
|
+
completed: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
109
172
|
export interface ClaudeControlRequest {
|
|
110
173
|
type: "control_request";
|
|
111
174
|
request_id: string;
|
|
@@ -120,6 +183,7 @@ export type NdjsonMessage =
|
|
|
120
183
|
| ClaudeStreamEventMessage
|
|
121
184
|
| ClaudeResultMessage
|
|
122
185
|
| ClaudeSystemMessage
|
|
186
|
+
| ClaudeTaskEvent
|
|
123
187
|
| ClaudeControlRequest
|
|
124
188
|
| ClaudeAssistantEnvelope
|
|
125
189
|
| ClaudeUserEnvelope
|