@norman-else/dsh-claude 0.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.
package/lib/index.mjs ADDED
@@ -0,0 +1,2272 @@
1
+ import { a as latestClaudeTasks, b as isClaudePresetId, c as normalizeTasksEvent, d as CLAUDE_ACTIVITY_EVENT, f as CLAUDE_CODE_PRESET_ID, g as CLAUDE_PROJECTION_PATH, h as CLAUDE_DOCTOR_PATH, i as latestClaudeSessionBinding, l as redactText, m as CLAUDE_CODE_PROVIDER_IDS, n as currentClaudeActivityCursor, o as normalizeActivity, p as CLAUDE_CODE_PROVIDER, r as latestClaudeContextUsage, s as normalizeContextUsage, t as boundText, u as safeDetail, y as TASK_TOOL_NAMES } from "./events-qlmU1KrH.mjs";
2
+ import { a as ClaudeCommandBridge, i as CLAUDE_COMMANDS_SERVICE, r as dynamicPresenterDefinition, t as CLAUDE_PRESENTER_NAMES } from "./presenters-DbfG-9KQ.mjs";
3
+ import { a as runClaudeDoctor, i as resolveClaudeExecutable, t as ensureManagedPreset } from "./preset-installer-DUVN1J6P.mjs";
4
+ import z from "@deepseek-ai/schemastery";
5
+ import { CallId, LlmAdapter, ReasoningEffortId, createToolResultMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
6
+ import { randomUUID } from "node:crypto";
7
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
8
+ import { join } from "node:path";
9
+ import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
10
+ import { query } from "@anthropic-ai/claude-agent-sdk";
11
+ import { EventEmitter } from "node:events";
12
+ import { DSH_ENV_PREFIX, SENSITIVE_ENV_PATTERN } from "@deepseek-ai/dsh-subprocess";
13
+ //#region src/sidecar.ts
14
+ const SIDECAR_SCHEMA_VERSION = 1;
15
+ const MAX_ACTIVITIES = 1e4;
16
+ function emptyProjection() {
17
+ return {
18
+ schemaVersion: SIDECAR_SCHEMA_VERSION,
19
+ revision: 0,
20
+ activities: []
21
+ };
22
+ }
23
+ function record$1(value) {
24
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
25
+ }
26
+ function finiteInteger(value) {
27
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
28
+ }
29
+ function string$1(value, max) {
30
+ return typeof value === "string" && value.length > 0 && value.length <= max;
31
+ }
32
+ function binding(value) {
33
+ const input = record$1(value);
34
+ if (input === void 0 || !string$1(input.claudeSessionId, 512) || !string$1(input.sdkVersion, 128) || !string$1(input.cwd, 4096) || input.cliVersion !== void 0 && !string$1(input.cliVersion, 128)) return void 0;
35
+ return {
36
+ claudeSessionId: input.claudeSessionId,
37
+ sdkVersion: input.sdkVersion,
38
+ cwd: input.cwd,
39
+ ...input.cliVersion === void 0 ? {} : { cliVersion: input.cliVersion }
40
+ };
41
+ }
42
+ const ACTIVITY_KINDS = /* @__PURE__ */ new Set([
43
+ "status",
44
+ "thinking",
45
+ "tool-call",
46
+ "tool-result",
47
+ "permission",
48
+ "subagent",
49
+ "usage",
50
+ "warning",
51
+ "error"
52
+ ]);
53
+ const ACTIVITY_PHASES = /* @__PURE__ */ new Set([
54
+ "started",
55
+ "updated",
56
+ "completed",
57
+ "denied",
58
+ "failed"
59
+ ]);
60
+ function activity(value) {
61
+ const input = record$1(value);
62
+ if (input === void 0 || !finiteInteger(input.turn) || !finiteInteger(input.step) || !finiteInteger(input.ordinal) || typeof input.kind !== "string" || !ACTIVITY_KINDS.has(input.kind) || input.phase !== void 0 && (typeof input.phase !== "string" || !ACTIVITY_PHASES.has(input.phase))) return void 0;
63
+ return normalizeActivity(input);
64
+ }
65
+ function contextUsage(value) {
66
+ const input = record$1(value);
67
+ if (input === void 0 || !Array.isArray(input.categories)) return void 0;
68
+ return normalizeContextUsage(input);
69
+ }
70
+ function tasks(value) {
71
+ const input = record$1(value);
72
+ if (input === void 0 || !Array.isArray(input.tasks)) return void 0;
73
+ return normalizeTasksEvent(input.tasks);
74
+ }
75
+ function parseClaudeSidecar(value) {
76
+ const input = record$1(value);
77
+ if (input === void 0 || input.schemaVersion !== SIDECAR_SCHEMA_VERSION || !finiteInteger(input.revision) || !Array.isArray(input.activities) || input.activities.length > MAX_ACTIVITIES) throw new Error("dsh-claude: invalid sidecar document");
78
+ const activities = input.activities.map(activity);
79
+ if (activities.some((item) => item === void 0)) throw new Error("dsh-claude: invalid sidecar activity");
80
+ const parsedBinding = input.binding === void 0 ? void 0 : binding(input.binding);
81
+ const parsedUsage = input.contextUsage === void 0 ? void 0 : contextUsage(input.contextUsage);
82
+ const parsedTasks = input.tasks === void 0 ? void 0 : tasks(input.tasks);
83
+ if (input.binding !== void 0 && parsedBinding === void 0 || input.contextUsage !== void 0 && parsedUsage === void 0 || input.tasks !== void 0 && parsedTasks === void 0) throw new Error("dsh-claude: invalid sidecar projection");
84
+ return {
85
+ schemaVersion: SIDECAR_SCHEMA_VERSION,
86
+ revision: input.revision,
87
+ activities,
88
+ ...parsedBinding === void 0 ? {} : { binding: parsedBinding },
89
+ ...parsedUsage === void 0 ? {} : { contextUsage: parsedUsage },
90
+ ...parsedTasks === void 0 ? {} : { tasks: parsedTasks }
91
+ };
92
+ }
93
+ function compareActivity(left, right) {
94
+ return left.turn - right.turn || left.step - right.step || left.ordinal - right.ordinal;
95
+ }
96
+ function activityKey(value) {
97
+ return `${value.turn}:${value.step}:${value.ordinal}`;
98
+ }
99
+ function mergeActivities(existing, additions) {
100
+ const merged = new Map(existing.map((item) => [activityKey(item), item]));
101
+ for (const item of additions) merged.set(activityKey(item), item);
102
+ return [...merged.values()].sort(compareActivity).slice(-1e4);
103
+ }
104
+ function normalizeBinding(input) {
105
+ return {
106
+ claudeSessionId: redactText(input.claudeSessionId, 512),
107
+ sdkVersion: redactText(input.sdkVersion ?? "0.3.233", 128),
108
+ cwd: redactText(input.cwd, 4096),
109
+ ...input.cliVersion === void 0 ? {} : { cliVersion: redactText(input.cliVersion, 128) }
110
+ };
111
+ }
112
+ var ClaudeSidecarRepository = class {
113
+ root;
114
+ legacyRoot;
115
+ #pending = /* @__PURE__ */ new Map();
116
+ constructor(options = {}) {
117
+ this.root = options.root ?? dshHomePath("plugins", "dsh-claude", "sessions");
118
+ this.legacyRoot = options.legacyRoot ?? (options.root === void 0 ? dshHomePath("plugins", "dsh-claude-code", "sessions") : void 0);
119
+ }
120
+ async read(sessionId) {
121
+ await this.#pending.get(sessionId)?.catch(() => void 0);
122
+ return this.#readNow(sessionId);
123
+ }
124
+ writeBinding(sessionId, value) {
125
+ const normalized = normalizeBinding(value);
126
+ return this.#update(sessionId, (current) => ({
127
+ ...current,
128
+ binding: normalized
129
+ }));
130
+ }
131
+ appendActivity(sessionId, value) {
132
+ const normalized = normalizeActivity(value);
133
+ return this.#update(sessionId, (current) => ({
134
+ ...current,
135
+ activities: mergeActivities(current.activities, [normalized])
136
+ }));
137
+ }
138
+ writeContextUsage(sessionId, value) {
139
+ const normalized = normalizeContextUsage(value);
140
+ return this.#update(sessionId, (current) => ({
141
+ ...current,
142
+ contextUsage: normalized
143
+ }));
144
+ }
145
+ writeTasks(sessionId, value) {
146
+ const normalized = normalizeTasksEvent(value);
147
+ return this.#update(sessionId, (current) => ({
148
+ ...current,
149
+ tasks: normalized
150
+ }));
151
+ }
152
+ importLegacy(sessionId, events) {
153
+ const importedActivities = events.filter((event) => event.type === CLAUDE_ACTIVITY_EVENT).map((event) => activity(event.data)).filter((item) => item !== void 0);
154
+ const importedBinding = latestClaudeSessionBinding(events);
155
+ const importedUsage = latestClaudeContextUsage(events);
156
+ const importedTasks = latestClaudeTasks(events);
157
+ return this.#update(sessionId, (current) => ({
158
+ ...current,
159
+ activities: mergeActivities(importedActivities, current.activities),
160
+ ...current.binding !== void 0 || importedBinding === void 0 ? {} : { binding: normalizeBinding(importedBinding) },
161
+ ...current.contextUsage !== void 0 || importedUsage === void 0 ? {} : { contextUsage: normalizeContextUsage(importedUsage) },
162
+ ...current.tasks !== void 0 || importedTasks === void 0 ? {} : { tasks: normalizeTasksEvent(importedTasks.tasks) }
163
+ }), true);
164
+ }
165
+ #path(sessionId, root = this.root) {
166
+ if (sessionId.length === 0 || sessionId.length > 1024) throw new Error("dsh-claude: invalid session id");
167
+ return join(root, `${Buffer.from(sessionId).toString("base64url")}.json`);
168
+ }
169
+ #update(sessionId, change, skipUnchanged = false) {
170
+ const operation = (this.#pending.get(sessionId) ?? Promise.resolve()).catch(() => void 0).then(async () => {
171
+ const current = await this.#readNow(sessionId);
172
+ const changed = parseClaudeSidecar({
173
+ ...change(current),
174
+ schemaVersion: SIDECAR_SCHEMA_VERSION,
175
+ revision: current.revision
176
+ });
177
+ if (skipUnchanged && JSON.stringify(changed) === JSON.stringify(current)) return current;
178
+ const next = {
179
+ ...changed,
180
+ revision: current.revision + 1
181
+ };
182
+ await this.#writeNow(sessionId, next);
183
+ return next;
184
+ });
185
+ this.#pending.set(sessionId, operation);
186
+ operation.finally(() => {
187
+ if (this.#pending.get(sessionId) === operation) this.#pending.delete(sessionId);
188
+ }).catch(() => void 0);
189
+ return operation;
190
+ }
191
+ async #readNow(sessionId) {
192
+ try {
193
+ return parseClaudeSidecar(JSON.parse(await readFile(this.#path(sessionId), "utf8")));
194
+ } catch (error) {
195
+ if (error.code !== "ENOENT") throw error;
196
+ }
197
+ if (this.legacyRoot === void 0) return emptyProjection();
198
+ try {
199
+ return parseClaudeSidecar(JSON.parse(await readFile(this.#path(sessionId, this.legacyRoot), "utf8")));
200
+ } catch (error) {
201
+ if (error.code === "ENOENT") return emptyProjection();
202
+ throw error;
203
+ }
204
+ }
205
+ async #writeNow(sessionId, projection) {
206
+ await mkdir(this.root, {
207
+ recursive: true,
208
+ mode: 448
209
+ });
210
+ await chmod(this.root, 448);
211
+ const target = this.#path(sessionId);
212
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
213
+ try {
214
+ await writeFile(temporary, `${JSON.stringify(projection)}\n`, {
215
+ mode: 384,
216
+ flag: "wx"
217
+ });
218
+ await chmod(temporary, 384);
219
+ await rename(temporary, target);
220
+ await chmod(target, 384);
221
+ } finally {
222
+ await rm(temporary, { force: true }).catch(() => void 0);
223
+ }
224
+ }
225
+ };
226
+ //#endregion
227
+ //#region src/async-queue.ts
228
+ var AsyncQueueClosedError = class extends Error {
229
+ constructor() {
230
+ super("Async queue is closed");
231
+ this.name = "AsyncQueueClosedError";
232
+ }
233
+ };
234
+ var AsyncQueue = class {
235
+ #values = [];
236
+ #pending = [];
237
+ #closed = false;
238
+ #failure;
239
+ get closed() {
240
+ return this.#closed;
241
+ }
242
+ push(value) {
243
+ if (this.#closed) throw new AsyncQueueClosedError();
244
+ const waiter = this.#pending.shift();
245
+ if (waiter !== void 0) waiter.resolve({
246
+ value,
247
+ done: false
248
+ });
249
+ else this.#values.push(value);
250
+ }
251
+ close() {
252
+ if (this.#closed) return;
253
+ this.#closed = true;
254
+ for (const waiter of this.#pending.splice(0)) waiter.resolve({
255
+ value: void 0,
256
+ done: true
257
+ });
258
+ }
259
+ fail(error) {
260
+ if (this.#closed) return;
261
+ this.#closed = true;
262
+ this.#failure = error;
263
+ for (const waiter of this.#pending.splice(0)) waiter.reject(error);
264
+ }
265
+ discard(error) {
266
+ this.#values.splice(0);
267
+ if (this.#closed) return;
268
+ this.#closed = true;
269
+ if (error === void 0) {
270
+ for (const waiter of this.#pending.splice(0)) waiter.resolve({
271
+ value: void 0,
272
+ done: true
273
+ });
274
+ return;
275
+ }
276
+ this.#failure = error;
277
+ for (const waiter of this.#pending.splice(0)) waiter.resolve({
278
+ value: void 0,
279
+ done: true
280
+ });
281
+ }
282
+ next() {
283
+ const value = this.#values.shift();
284
+ if (value !== void 0) return Promise.resolve({
285
+ value,
286
+ done: false
287
+ });
288
+ if (this.#closed) return this.#failure === void 0 ? Promise.resolve({
289
+ value: void 0,
290
+ done: true
291
+ }) : Promise.reject(this.#failure);
292
+ return new Promise((resolve, reject) => {
293
+ this.#pending.push({
294
+ resolve,
295
+ reject
296
+ });
297
+ });
298
+ }
299
+ return() {
300
+ this.close();
301
+ return Promise.resolve({
302
+ value: void 0,
303
+ done: true
304
+ });
305
+ }
306
+ [Symbol.asyncIterator]() {
307
+ return this;
308
+ }
309
+ };
310
+ //#endregion
311
+ //#region src/permission.ts
312
+ function denialMessage(outcome) {
313
+ switch (outcome) {
314
+ case "rejected": return "The user rejected this action in DeepSeek Harness.";
315
+ case "cancelled": return "The permission request was cancelled in DeepSeek Harness.";
316
+ case "unavailable": return "No DeepSeek Harness approval surface was available; the action was denied.";
317
+ case "allowed-once": return "";
318
+ }
319
+ }
320
+ function permissionReason(toolName, input, options) {
321
+ const prompt = options.title ?? options.description ?? options.decisionReason ?? `Claude Code wants to use ${toolName}.`;
322
+ const detail = safeDetail(input);
323
+ return boundText(detail === void 0 ? prompt : `${prompt}\nInput: ${detail}`, 1200);
324
+ }
325
+ function mapApprovalOutcome(outcome, input, toolUseID) {
326
+ if (outcome === "allowed-once") return {
327
+ behavior: "allow",
328
+ updatedInput: input,
329
+ toolUseID,
330
+ decisionClassification: "user_temporary"
331
+ };
332
+ return {
333
+ behavior: "deny",
334
+ message: denialMessage(outcome),
335
+ toolUseID,
336
+ decisionClassification: "user_reject"
337
+ };
338
+ }
339
+ function createPermissionBridge(approval, activeContext) {
340
+ return async (toolName, input, options) => {
341
+ const active = activeContext();
342
+ if (active === void 0) return {
343
+ behavior: "deny",
344
+ message: "No active DeepSeek Harness turn owns this Claude Code action.",
345
+ toolUseID: options.toolUseID,
346
+ decisionClassification: "user_reject"
347
+ };
348
+ active.markActivity?.();
349
+ const reason = permissionReason(toolName, input, options);
350
+ try {
351
+ await active.appendActivity({
352
+ kind: "permission",
353
+ phase: "started",
354
+ toolUseId: options.toolUseID,
355
+ toolName,
356
+ title: options.displayName ?? toolName,
357
+ summary: options.title ?? options.description ?? reason,
358
+ detail: input
359
+ });
360
+ const outcome = await approval.request({
361
+ agent: active.agent,
362
+ toolName,
363
+ reason,
364
+ signal: options.signal
365
+ });
366
+ const result = mapApprovalOutcome(outcome, input, options.toolUseID);
367
+ if (result.behavior === "deny") active.recordDenial?.(options.toolUseID);
368
+ await active.appendActivity({
369
+ kind: "permission",
370
+ phase: outcome === "allowed-once" ? "completed" : "denied",
371
+ toolUseId: options.toolUseID,
372
+ toolName,
373
+ title: options.displayName ?? toolName,
374
+ summary: outcome === "allowed-once" ? "Allowed once in DeepSeek Harness" : denialMessage(outcome)
375
+ });
376
+ return result;
377
+ } catch (error) {
378
+ const message = options.signal.aborted ? "The permission request was cancelled in DeepSeek Harness." : "DeepSeek Harness could not record or answer the permission request; the action was denied.";
379
+ try {
380
+ await active.appendActivity({
381
+ kind: "permission",
382
+ phase: "failed",
383
+ toolUseId: options.toolUseID,
384
+ toolName,
385
+ title: options.displayName ?? toolName,
386
+ summary: message,
387
+ isError: true,
388
+ detail: error
389
+ });
390
+ } catch {}
391
+ return {
392
+ behavior: "deny",
393
+ message,
394
+ toolUseID: options.toolUseID,
395
+ decisionClassification: "user_reject"
396
+ };
397
+ }
398
+ };
399
+ }
400
+ //#endregion
401
+ //#region src/sdk-messages.ts
402
+ function record(value) {
403
+ return value !== null && typeof value === "object" ? value : void 0;
404
+ }
405
+ function string(value) {
406
+ return typeof value === "string" ? value : void 0;
407
+ }
408
+ function taskUsageOf(usage) {
409
+ if (usage === void 0) return void 0;
410
+ const normalized = {};
411
+ if (typeof usage.total_tokens === "number") normalized.totalTokens = usage.total_tokens;
412
+ if (typeof usage.tool_uses === "number") normalized.toolUses = usage.tool_uses;
413
+ if (typeof usage.duration_ms === "number") normalized.durationMs = usage.duration_ms;
414
+ return Object.keys(normalized).length === 0 ? void 0 : normalized;
415
+ }
416
+ function resultUsage(message) {
417
+ const usage = record(message.usage);
418
+ const normalized = {};
419
+ if (usage !== void 0) {
420
+ if (typeof usage.input_tokens === "number") normalized.inputTokens = usage.input_tokens;
421
+ if (typeof usage.output_tokens === "number") normalized.outputTokens = usage.output_tokens;
422
+ if (typeof usage.cache_read_input_tokens === "number") normalized.cacheReadTokens = usage.cache_read_input_tokens;
423
+ if (typeof usage.cache_creation_input_tokens === "number") normalized.cacheCreationTokens = usage.cache_creation_input_tokens;
424
+ }
425
+ if (typeof message.total_cost_usd === "number") normalized.cumulativeCostUsd = message.total_cost_usd;
426
+ return normalized;
427
+ }
428
+ function normalizeAssistant(message) {
429
+ const content = record(message.message)?.content;
430
+ if (!Array.isArray(content)) return [{
431
+ kind: "protocol-error",
432
+ title: "Malformed Claude assistant message",
433
+ detail: message
434
+ }];
435
+ const parentToolUseId = string(message.parent_tool_use_id);
436
+ const normalized = [];
437
+ for (const item of content) {
438
+ const block = record(item);
439
+ if (block === void 0) continue;
440
+ if (block.type === "text") {
441
+ const text = string(block.text);
442
+ if (text !== void 0 && text.length > 0) normalized.push({
443
+ kind: "assistant-text",
444
+ text,
445
+ ...parentToolUseId === void 0 ? {} : { parentToolUseId }
446
+ });
447
+ } else if (block.type === "thinking") {
448
+ const text = string(block.thinking);
449
+ if (text !== void 0 && text.length > 0) normalized.push({
450
+ kind: "thinking",
451
+ text,
452
+ phase: "completed",
453
+ ...parentToolUseId === void 0 ? {} : { parentToolUseId }
454
+ });
455
+ } else if (block.type === "tool_use") {
456
+ const toolUseId = string(block.id);
457
+ const toolName = string(block.name);
458
+ if (toolUseId !== void 0 && toolName !== void 0) normalized.push({
459
+ kind: "tool-call",
460
+ toolUseId,
461
+ toolName,
462
+ input: block.input,
463
+ ...parentToolUseId === void 0 ? {} : { parentToolUseId }
464
+ });
465
+ }
466
+ }
467
+ return normalized;
468
+ }
469
+ function normalizeUser(message) {
470
+ const content = record(message.message)?.content;
471
+ if (!Array.isArray(content)) return [{
472
+ kind: "protocol-error",
473
+ title: "Malformed Claude user message",
474
+ detail: message
475
+ }];
476
+ const parentToolUseId = string(message.parent_tool_use_id);
477
+ const normalized = [];
478
+ for (const item of content) {
479
+ const block = record(item);
480
+ if (block?.type !== "tool_result") continue;
481
+ const toolUseId = string(block.tool_use_id);
482
+ if (toolUseId === void 0) continue;
483
+ normalized.push({
484
+ kind: "tool-result",
485
+ toolUseId,
486
+ output: message.tool_use_result ?? block.content,
487
+ isError: block.is_error === true,
488
+ ...parentToolUseId === void 0 ? {} : { parentToolUseId }
489
+ });
490
+ }
491
+ return normalized;
492
+ }
493
+ function normalizeSystem(message) {
494
+ const subtype = string(message.subtype);
495
+ if (subtype === "init") {
496
+ const sessionId = string(message.session_id);
497
+ const cliVersion = string(message.claude_code_version);
498
+ const cwd = string(message.cwd);
499
+ return sessionId !== void 0 && cliVersion !== void 0 && cwd !== void 0 ? [{
500
+ kind: "init",
501
+ sessionId,
502
+ cliVersion,
503
+ cwd
504
+ }] : [{
505
+ kind: "protocol-error",
506
+ title: "Malformed Claude initialization message",
507
+ detail: message
508
+ }];
509
+ }
510
+ if (subtype === "status") {
511
+ const status = message.status;
512
+ if (status === null) return [{
513
+ kind: "status",
514
+ title: "Claude Code is ready"
515
+ }];
516
+ return [{
517
+ kind: "status",
518
+ title: `Claude Code ${String(status)}`,
519
+ detail: message
520
+ }];
521
+ }
522
+ if (subtype === "session_state_changed") return [{
523
+ kind: "status",
524
+ title: `Claude session ${String(message.state)}`
525
+ }];
526
+ if (subtype === "permission_denied") {
527
+ const toolUseId = string(message.tool_use_id);
528
+ const toolName = string(message.tool_name);
529
+ if (toolUseId !== void 0 && toolName !== void 0) return [{
530
+ kind: "permission-denied",
531
+ toolUseId,
532
+ toolName,
533
+ summary: string(message.message) ?? "Claude Code denied the action"
534
+ }];
535
+ }
536
+ if (subtype === "task_started") {
537
+ const taskId = string(message.task_id);
538
+ const description = string(message.description);
539
+ const subagentType = string(message.subagent_type);
540
+ const taskType = string(message.task_type);
541
+ return [{
542
+ kind: "subagent",
543
+ title: description ?? taskId ?? "Claude subagent started",
544
+ phase: "started",
545
+ detail: message,
546
+ ...taskId === void 0 ? {} : { taskId },
547
+ taskStatus: "running",
548
+ ...description === void 0 ? {} : { description },
549
+ ...subagentType === void 0 ? {} : { subagentType },
550
+ ...taskType === void 0 ? {} : { taskType },
551
+ ...message.skip_transcript === true ? { skipTranscript: true } : {}
552
+ }];
553
+ }
554
+ if (subtype === "task_progress") {
555
+ const taskId = string(message.task_id);
556
+ const description = string(message.description);
557
+ const summary = string(message.summary);
558
+ const subagentType = string(message.subagent_type);
559
+ const lastToolName = string(message.last_tool_name);
560
+ const usage = taskUsageOf(record(message.usage));
561
+ return [{
562
+ kind: "subagent",
563
+ title: summary ?? description ?? "Claude subagent update",
564
+ phase: "updated",
565
+ detail: message,
566
+ ...taskId === void 0 ? {} : { taskId },
567
+ taskStatus: "running",
568
+ ...description === void 0 ? {} : { description },
569
+ ...subagentType === void 0 ? {} : { subagentType },
570
+ ...lastToolName === void 0 ? {} : { lastToolName },
571
+ ...summary === void 0 ? {} : { summary },
572
+ ...usage === void 0 ? {} : { usage }
573
+ }];
574
+ }
575
+ if (subtype === "task_updated") {
576
+ const patch = record(message.patch);
577
+ const status = string(patch?.status);
578
+ const taskId = string(message.task_id);
579
+ const description = string(patch?.description);
580
+ const error = string(patch?.error);
581
+ const taskStatus = status === void 0 ? void 0 : status === "killed" ? "killed" : status === "completed" ? "completed" : status === "failed" ? "failed" : "running";
582
+ return [{
583
+ kind: "subagent",
584
+ title: description ?? "Claude subagent update",
585
+ phase: status === "failed" || status === "killed" ? "failed" : status === "completed" ? "completed" : "updated",
586
+ detail: message,
587
+ ...taskId === void 0 ? {} : { taskId },
588
+ ...taskStatus === void 0 ? {} : { taskStatus },
589
+ ...description === void 0 ? {} : { description },
590
+ ...error === void 0 ? {} : { summary: error }
591
+ }];
592
+ }
593
+ if (subtype === "task_notification") {
594
+ const failed = message.status === "failed";
595
+ const stopped = message.status === "stopped" || message.status === "cancelled";
596
+ const taskId = string(message.task_id);
597
+ const summary = string(message.summary);
598
+ const taskStatus = failed ? "failed" : stopped ? "stopped" : "completed";
599
+ const usage = taskUsageOf(record(message.usage));
600
+ return [{
601
+ kind: "subagent",
602
+ title: summary ?? taskId ?? "Claude subagent finished",
603
+ phase: failed || stopped ? "failed" : "completed",
604
+ detail: message,
605
+ ...taskId === void 0 ? {} : { taskId },
606
+ taskStatus,
607
+ ...summary === void 0 ? {} : { summary },
608
+ ...usage === void 0 ? {} : { usage }
609
+ }];
610
+ }
611
+ if (subtype === "background_tasks_changed") return [{
612
+ kind: "background-tasks",
613
+ tasks: (Array.isArray(message.tasks) ? message.tasks : []).flatMap((item) => {
614
+ const entry = record(item);
615
+ const taskId = string(entry?.task_id);
616
+ const description = string(entry?.description);
617
+ const taskType = string(entry?.task_type);
618
+ if (taskId === void 0 || description === void 0) return [];
619
+ return [{
620
+ taskId,
621
+ description,
622
+ ...taskType === void 0 ? {} : { taskType }
623
+ }];
624
+ })
625
+ }];
626
+ if (subtype === "api_retry") return [{
627
+ kind: "warning",
628
+ title: "Claude API retry",
629
+ detail: message
630
+ }];
631
+ if (subtype === "informational" || subtype === "notification" || subtype === "local_command_output") return [{
632
+ kind: message.level === "warning" ? "warning" : "status",
633
+ title: string(message.content) ?? string(message.text) ?? "Claude Code notice",
634
+ detail: message
635
+ }];
636
+ if (subtype?.startsWith("hook_") === true || subtype === "plugin_install") return [{
637
+ kind: "status",
638
+ title: `Claude Code ${subtype.replaceAll("_", " ")}`,
639
+ detail: message
640
+ }];
641
+ if (subtype !== void 0) return [{
642
+ kind: "status",
643
+ title: `Claude Code ${subtype.replaceAll("_", " ")}`,
644
+ detail: message
645
+ }];
646
+ return [];
647
+ }
648
+ const RESULT_ERROR_SUBTYPES = /* @__PURE__ */ new Set([
649
+ "error_during_execution",
650
+ "error_max_turns",
651
+ "error_max_budget_usd",
652
+ "error_max_structured_output_retries"
653
+ ]);
654
+ function normalizeSdkMessage(message) {
655
+ const value = message;
656
+ if (value.type === "stream_event") {
657
+ const event = record(value.event);
658
+ const parentToolUseId = string(value.parent_tool_use_id);
659
+ if (event?.type === "content_block_delta") {
660
+ const delta = record(event.delta);
661
+ if (delta?.type === "text_delta") {
662
+ const text = string(delta.text);
663
+ return text === void 0 ? [] : [{
664
+ kind: "text-delta",
665
+ text,
666
+ ...parentToolUseId === void 0 ? {} : { parentToolUseId }
667
+ }];
668
+ }
669
+ if (delta?.type === "thinking_delta") {
670
+ const text = string(delta.thinking);
671
+ return text === void 0 ? [] : [{
672
+ kind: "thinking",
673
+ text,
674
+ phase: "updated",
675
+ ...parentToolUseId === void 0 ? {} : { parentToolUseId }
676
+ }];
677
+ }
678
+ }
679
+ return [];
680
+ }
681
+ if (value.type === "assistant") return normalizeAssistant(value);
682
+ if (value.type === "user") return normalizeUser(value);
683
+ if (value.type === "system") return normalizeSystem(value);
684
+ if (value.type === "result") {
685
+ const sessionId = string(value.session_id);
686
+ if (sessionId === void 0 || value.subtype !== "success" && !RESULT_ERROR_SUBTYPES.has(String(value.subtype))) return [{
687
+ kind: "protocol-error",
688
+ title: "Malformed Claude result message",
689
+ detail: value
690
+ }];
691
+ const success = value.subtype === "success" && value.is_error !== true;
692
+ const errors = Array.isArray(value.errors) ? value.errors.filter((item) => typeof item === "string") : void 0;
693
+ const terminalReason = string(value.terminal_reason);
694
+ const userMessageUuid = string(value.user_message_uuid);
695
+ const permissionDenials = Array.isArray(value.permission_denials) ? value.permission_denials.map((item) => record(item)).filter((item) => item !== void 0).map((item) => {
696
+ const toolName = string(item.tool_name);
697
+ const toolUseId = string(item.tool_use_id);
698
+ return toolName === void 0 || toolUseId === void 0 ? void 0 : {
699
+ toolName,
700
+ toolUseId
701
+ };
702
+ }).filter((item) => item !== void 0).slice(0, 40) : void 0;
703
+ return [{
704
+ kind: "result",
705
+ success,
706
+ ...success && typeof value.result === "string" ? { text: value.result } : {},
707
+ ...errors === void 0 ? {} : { errors },
708
+ ...terminalReason === void 0 ? {} : { terminalReason },
709
+ ...permissionDenials === void 0 || permissionDenials.length === 0 ? {} : { permissionDenials },
710
+ usage: resultUsage(value),
711
+ sessionId,
712
+ ...userMessageUuid === void 0 ? {} : { userMessageUuid }
713
+ }];
714
+ }
715
+ if (value.type === "auth_status") return [{
716
+ kind: value.error === void 0 ? "status" : "warning",
717
+ title: value.error === void 0 ? "Claude authentication status changed" : "Claude authentication failed",
718
+ detail: value.error ?? value.output
719
+ }];
720
+ if (value.type === "rate_limit_event") {
721
+ const status = string(record(value.rate_limit_info)?.status);
722
+ const blocking = status !== void 0 && status !== "allowed";
723
+ return [{
724
+ kind: blocking ? "warning" : "status",
725
+ title: blocking ? "Claude rate limit is blocking requests" : "Claude rate limit status changed",
726
+ detail: value.rate_limit_info
727
+ }];
728
+ }
729
+ return [{
730
+ kind: "unknown",
731
+ title: `Unknown Claude SDK message: ${String(value.type)}`,
732
+ detail: value
733
+ }];
734
+ }
735
+ //#endregion
736
+ //#region src/spawn.ts
737
+ const CLAUDE_PROCESS_GRACE_MS = 2e3;
738
+ const CLAUDE_STDERR_TAIL_BYTES = 32768;
739
+ const ADDITIONAL_SENSITIVE_ENV_PATTERN = /(?:authorization|cookie|credential|database[_-]?url|private[_-]?key|netrc)/iu;
740
+ function scrubClaudeSpawnEnv(env) {
741
+ const safe = {};
742
+ for (const [key, value] of Object.entries(env)) {
743
+ if (value === void 0) continue;
744
+ if (key.toUpperCase().startsWith(DSH_ENV_PREFIX)) continue;
745
+ if (SENSITIVE_ENV_PATTERN.test(key)) continue;
746
+ if (ADDITIONAL_SENSITIVE_ENV_PATTERN.test(key)) continue;
747
+ safe[key] = value;
748
+ }
749
+ return safe;
750
+ }
751
+ var ManagedClaudeProcess = class extends EventEmitter {
752
+ stdin;
753
+ stdout;
754
+ handle;
755
+ #killed = false;
756
+ #exitCode = null;
757
+ #signalCode = null;
758
+ constructor(handle) {
759
+ super();
760
+ if (handle.stdin === void 0 || handle.stdout === void 0) throw new Error("dsh-claude: managed Claude process requires piped stdin/stdout");
761
+ this.handle = handle;
762
+ this.stdin = handle.stdin;
763
+ this.stdout = handle.stdout;
764
+ handle.done.then((outcome) => {
765
+ this.#exitCode = outcome.exitCode;
766
+ this.#signalCode = outcome.signal;
767
+ this.emit("exit", outcome.exitCode, outcome.signal);
768
+ }, (error) => {
769
+ this.emit("error", error instanceof Error ? error : new Error(String(error)));
770
+ });
771
+ }
772
+ get killed() {
773
+ return this.#killed;
774
+ }
775
+ get exitCode() {
776
+ return this.#exitCode;
777
+ }
778
+ get signalCode() {
779
+ return this.#signalCode;
780
+ }
781
+ kill(signal) {
782
+ if (this.#exitCode !== null || this.#signalCode !== null) return false;
783
+ this.#killed = true;
784
+ this.handle.terminate();
785
+ return true;
786
+ }
787
+ stderrTail() {
788
+ return this.handle.collected.stderr?.readFrom(0).text ?? "";
789
+ }
790
+ };
791
+ function createManagedClaudeSpawner(runtime, executablePath, observe) {
792
+ return (options) => {
793
+ if (options.command !== executablePath) throw new Error(`dsh-claude: SDK requested unexpected executable ${JSON.stringify(options.command)}`);
794
+ const managed = new ManagedClaudeProcess(runtime.spawn({
795
+ argv: [executablePath, ...options.args],
796
+ cwd: options.cwd ?? process.cwd(),
797
+ stdio: {
798
+ stdin: "pipe",
799
+ stdout: "pipe",
800
+ stderr: { maxBytes: CLAUDE_STDERR_TAIL_BYTES }
801
+ },
802
+ graceMs: CLAUDE_PROCESS_GRACE_MS,
803
+ signal: options.signal,
804
+ env: scrubClaudeSpawnEnv(options.env)
805
+ }));
806
+ observe?.(managed, options);
807
+ return managed;
808
+ };
809
+ }
810
+ //#endregion
811
+ //#region src/supervisor.ts
812
+ const CLAUDE_INITIALIZATION_TIMEOUT_MS = 3e4;
813
+ /** Control requests must settle; a wedged one must not clog the metadata chain. */
814
+ const CLAUDE_METADATA_TIMEOUT_MS = 15e3;
815
+ const CLAUDE_MODE_BY_SANDBOX = {
816
+ "read-only": "plan",
817
+ "workspace-write": "acceptEdits",
818
+ "danger-full-access": "bypassPermissions"
819
+ };
820
+ /** Fold DSH's native access selector into Claude Code's closest permission mode. */
821
+ function claudePermissionMode(events) {
822
+ for (let index = events.length - 1; index >= 0; index -= 1) {
823
+ const event = events[index];
824
+ if (event?.type !== "sandbox/mode") continue;
825
+ const mode = event.data.mode;
826
+ return typeof mode === "string" && mode in CLAUDE_MODE_BY_SANDBOX ? CLAUDE_MODE_BY_SANDBOX[mode] : "plan";
827
+ }
828
+ return "plan";
829
+ }
830
+ var ClaudeTurnBusyError = class extends Error {
831
+ constructor(sessionId) {
832
+ super(`Claude Code session ${sessionId} already has an active or interrupting turn`);
833
+ this.name = "ClaudeTurnBusyError";
834
+ }
835
+ };
836
+ var ClaudeOutcomeUnknownError = class extends Error {
837
+ constructor(message = "Claude Code exited after activity; side-effect outcome is unknown and the prompt was not replayed") {
838
+ super(message);
839
+ this.name = "ClaudeOutcomeUnknownError";
840
+ }
841
+ };
842
+ var ClaudeProtocolError = class extends Error {
843
+ constructor(message) {
844
+ super(message);
845
+ this.name = "ClaudeProtocolError";
846
+ }
847
+ };
848
+ var ClaudeProcessLimitError = class extends Error {
849
+ constructor(maxProcesses) {
850
+ super(`Claude Code process limit reached (${maxProcesses}) and no idle session can be evicted`);
851
+ this.name = "ClaudeProcessLimitError";
852
+ }
853
+ };
854
+ function abortFailure() {
855
+ const error = /* @__PURE__ */ new Error("Claude Code turn aborted");
856
+ error.name = "AbortError";
857
+ return error;
858
+ }
859
+ function signalAborted(signal) {
860
+ return signal?.aborted === true;
861
+ }
862
+ async function withTimeout(operation, timeoutMs, label) {
863
+ let timer;
864
+ try {
865
+ return await Promise.race([operation, new Promise((_resolve, reject) => {
866
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
867
+ timer.unref?.();
868
+ })]);
869
+ } finally {
870
+ if (timer !== void 0) clearTimeout(timer);
871
+ }
872
+ }
873
+ function sdkUserMessage(prompt, uuid) {
874
+ return {
875
+ type: "user",
876
+ message: {
877
+ role: "user",
878
+ content: prompt
879
+ },
880
+ parent_tool_use_id: null,
881
+ uuid
882
+ };
883
+ }
884
+ /** CLI ≥ 2.1.235 emits system/init only after its first stdin input, while
885
+ * the supervisor must see init before submitting any real turn. A local
886
+ * slash command nudges startup without costing a model call; its lifecycle
887
+ * messages are ignored because no DSH turn is active while they arrive. */
888
+ const CLAUDE_HANDSHAKE_PROMPT = "/status";
889
+ function usageSummary(usage) {
890
+ return `${usage.inputTokens ?? 0} input / ${usage.outputTokens ?? 0} output tokens${usage.cumulativeCostUsd === void 0 ? "" : ` · $${usage.cumulativeCostUsd.toFixed(4)} cumulative`}`;
891
+ }
892
+ function errorSummary(error) {
893
+ return error instanceof Error ? error.message : String(error);
894
+ }
895
+ /** Root-call activity summary; subagent dispatches lead with Claude's own task description. */
896
+ function rootCallSummary(toolName, input) {
897
+ if (TASK_TOOL_NAMES.has(toolName)) {
898
+ const description = input !== null && typeof input === "object" ? input.description : void 0;
899
+ if (typeof description === "string" && description.length > 0) return description;
900
+ return "Claude dispatched a subagent";
901
+ }
902
+ return `Claude called ${toolName}`;
903
+ }
904
+ var ClaudeSupervisor = class {
905
+ #entries = /* @__PURE__ */ new Map();
906
+ #runtime;
907
+ #approval;
908
+ #config;
909
+ #queryFactory;
910
+ #runDetached;
911
+ #sidecar;
912
+ #dynamicPresenterNames = /* @__PURE__ */ new WeakMap();
913
+ #contextWindows = /* @__PURE__ */ new Map();
914
+ #disposed = false;
915
+ #admissionGate = Promise.resolve();
916
+ constructor(dependencies) {
917
+ this.#runtime = dependencies.runtime;
918
+ this.#approval = dependencies.approval;
919
+ this.#config = dependencies.config;
920
+ this.#queryFactory = dependencies.queryFactory ?? ((params) => query(params));
921
+ this.#runDetached = dependencies.runDetached ?? ((operation) => operation());
922
+ this.#sidecar = dependencies.sidecar ?? new ClaudeSidecarRepository();
923
+ }
924
+ snapshots() {
925
+ return [...this.#entries.values()].map((entry) => ({
926
+ sessionId: entry.sessionId,
927
+ ...entry.claudeSessionId === void 0 ? {} : { claudeSessionId: entry.claudeSessionId },
928
+ state: entry.state,
929
+ cwd: entry.cwd,
930
+ model: entry.model,
931
+ ...entry.thinkingMode === void 0 ? {} : { thinkingMode: entry.thinkingMode },
932
+ lastUsedAt: entry.lastUsedAt,
933
+ ...entry.process === void 0 ? {} : { pid: entry.process.handle.pid }
934
+ }));
935
+ }
936
+ supportedCommands(agent, model = this.#config.defaultModel) {
937
+ return this.#runMetadata(agent, model, (query) => query.supportedCommands());
938
+ }
939
+ async contextUsage(agent, model = this.#config.defaultModel) {
940
+ const usage = await this.#runMetadata(agent, model, (query) => query.getContextUsage());
941
+ const contextWindow = usage.rawMaxTokens > 0 ? usage.rawMaxTokens : usage.maxTokens;
942
+ if (contextWindow > 0) {
943
+ this.#contextWindows.set(model, contextWindow);
944
+ this.#contextWindows.set(usage.model, contextWindow);
945
+ }
946
+ return usage;
947
+ }
948
+ contextWindow(model) {
949
+ return this.#contextWindows.get(model);
950
+ }
951
+ runTurn(request) {
952
+ const operation = this.#admissionGate.then(() => this.#runTurnAdmitted(request));
953
+ this.#admissionGate = operation.then(() => void 0, () => void 0);
954
+ return operation;
955
+ }
956
+ async #runTurnAdmitted(request) {
957
+ if (this.#disposed) throw new Error("dsh-claude: supervisor is disposed");
958
+ if (signalAborted(request.signal)) throw abortFailure();
959
+ const sessionId = request.agent.id;
960
+ let entry = this.#entries.get(sessionId);
961
+ if (entry?.state === "disposed" || entry?.state === "disconnected" || entry?.state === "outcome-unknown") {
962
+ this.#entries.delete(sessionId);
963
+ await this.#disposeEntry(entry);
964
+ entry = void 0;
965
+ }
966
+ if (entry === void 0) {
967
+ await this.#makeRoom();
968
+ entry = await this.#createEntry(request.agent, request.model ?? this.#config.defaultModel, request.thinkingMode);
969
+ this.#entries.set(sessionId, entry);
970
+ this.#armInitializationTimer(entry);
971
+ }
972
+ if (entry.ownerAgent !== request.agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
973
+ if (entry.active !== void 0 || entry.state === "interrupting") throw new ClaudeTurnBusyError(sessionId);
974
+ if (entry.idleTimer !== void 0) {
975
+ clearTimeout(entry.idleTimer);
976
+ entry.idleTimer = void 0;
977
+ }
978
+ const model = request.model ?? this.#config.defaultModel;
979
+ if (request.thinkingMode !== entry.thinkingMode) {
980
+ this.#entries.delete(sessionId);
981
+ await this.#disposeEntry(entry);
982
+ entry = await this.#createEntry(request.agent, model, request.thinkingMode);
983
+ this.#entries.set(sessionId, entry);
984
+ this.#armInitializationTimer(entry);
985
+ } else {
986
+ await this.#syncPermissionMode(entry);
987
+ if (model !== entry.model) {
988
+ await entry.query.setModel(model);
989
+ entry.model = model;
990
+ }
991
+ }
992
+ const promptUuid = randomUUID();
993
+ const cursor = currentClaudeActivityCursor(request.agent.session.events);
994
+ cursor.nextOrdinal = (await this.#sidecar.read(sessionId)).activities.reduce((next, activity) => activity.turn === cursor.turn && activity.step === cursor.step ? Math.max(next, activity.ordinal + 1) : next, 0);
995
+ const active = {
996
+ agent: request.agent,
997
+ cursor,
998
+ output: new AsyncQueue(),
999
+ promptUuid,
1000
+ sawActivity: false,
1001
+ sawTextDelta: false,
1002
+ text: "",
1003
+ thinking: "",
1004
+ aborted: false,
1005
+ deniedToolUseIds: /* @__PURE__ */ new Set(),
1006
+ callNames: /* @__PURE__ */ new Map(),
1007
+ ...request.signal === void 0 ? {} : { signal: request.signal }
1008
+ };
1009
+ entry.active = active;
1010
+ entry.state = "running";
1011
+ entry.lastUsedAt = Date.now();
1012
+ try {
1013
+ await this.#appendActivity(active, {
1014
+ kind: "status",
1015
+ phase: "started",
1016
+ title: "Claude Code turn started"
1017
+ });
1018
+ } catch (error) {
1019
+ active.output.fail(error);
1020
+ entry.active = void 0;
1021
+ if (this.#entries.get(sessionId) === entry) this.#entries.delete(sessionId);
1022
+ await this.#disposeEntry(entry);
1023
+ throw error;
1024
+ }
1025
+ if (signalAborted(request.signal)) {
1026
+ active.aborted = true;
1027
+ active.output.fail(abortFailure());
1028
+ await this.#appendActivity(active, {
1029
+ kind: "status",
1030
+ phase: "failed",
1031
+ title: "Claude Code turn cancelled before submission"
1032
+ });
1033
+ entry.active = void 0;
1034
+ entry.state = "idle";
1035
+ entry.lastUsedAt = Date.now();
1036
+ this.#armIdleTimer(entry);
1037
+ return active.output;
1038
+ }
1039
+ if (request.signal !== void 0) {
1040
+ const abortListener = () => {
1041
+ this.#interrupt(entry);
1042
+ };
1043
+ active.abortListener = abortListener;
1044
+ request.signal.addEventListener("abort", abortListener, { once: true });
1045
+ }
1046
+ entry.input.push(sdkUserMessage(request.prompt, promptUuid));
1047
+ return active.output;
1048
+ }
1049
+ #runMetadata(agent, model, operation) {
1050
+ const admitted = this.#admissionGate.then(async () => {
1051
+ if (this.#disposed) throw new Error("dsh-claude: supervisor is disposed");
1052
+ const entry = await this.#metadataEntry(agent, model);
1053
+ try {
1054
+ await withTimeout(this.#whenInitialized(entry), CLAUDE_INITIALIZATION_TIMEOUT_MS, "Claude metadata initialization");
1055
+ return await withTimeout(operation(entry.query, entry), CLAUDE_METADATA_TIMEOUT_MS, "Claude metadata request");
1056
+ } finally {
1057
+ entry.lastUsedAt = Date.now();
1058
+ if (entry.active === void 0 && entry.state === "idle") this.#armIdleTimer(entry);
1059
+ }
1060
+ });
1061
+ this.#admissionGate = admitted.then(() => void 0, () => void 0);
1062
+ return admitted;
1063
+ }
1064
+ #whenInitialized(entry) {
1065
+ if (entry.initialized) return Promise.resolve();
1066
+ if (entry.state === "disposed" || entry.state === "disconnected" || entry.state === "outcome-unknown") return Promise.reject(/* @__PURE__ */ new Error(`dsh-claude: session ${entry.sessionId} is ${entry.state}`));
1067
+ return new Promise((resolve, reject) => {
1068
+ entry.initWaiters.push((error) => error === void 0 ? resolve() : reject(error instanceof Error ? error : new Error(String(error))));
1069
+ });
1070
+ }
1071
+ async #metadataEntry(agent, model) {
1072
+ const sessionId = agent.id;
1073
+ let entry = this.#entries.get(sessionId);
1074
+ if (entry?.state === "disposed" || entry?.state === "disconnected" || entry?.state === "outcome-unknown") {
1075
+ this.#entries.delete(sessionId);
1076
+ await this.#disposeEntry(entry);
1077
+ entry = void 0;
1078
+ }
1079
+ if (entry === void 0) {
1080
+ await this.#makeRoom();
1081
+ entry = await this.#createEntry(agent, model);
1082
+ this.#entries.set(sessionId, entry);
1083
+ this.#armInitializationTimer(entry);
1084
+ }
1085
+ if (entry.ownerAgent !== agent) throw new Error(`dsh-claude: live agent identity changed for session ${sessionId}`);
1086
+ if (entry.active !== void 0 || entry.state === "interrupting") throw new ClaudeTurnBusyError(sessionId);
1087
+ if (entry.idleTimer !== void 0) {
1088
+ clearTimeout(entry.idleTimer);
1089
+ entry.idleTimer = void 0;
1090
+ }
1091
+ await this.#syncPermissionMode(entry);
1092
+ if (model !== entry.model) {
1093
+ await entry.query.setModel(model);
1094
+ entry.model = model;
1095
+ }
1096
+ return entry;
1097
+ }
1098
+ async #syncPermissionMode(entry) {
1099
+ const mode = claudePermissionMode(entry.ownerAgent.session.events);
1100
+ if (mode === entry.permissionMode) return;
1101
+ await entry.query.setPermissionMode(mode);
1102
+ entry.permissionMode = mode;
1103
+ }
1104
+ async disposeSession(sessionId) {
1105
+ const entry = this.#entries.get(sessionId);
1106
+ if (entry === void 0) return;
1107
+ this.#entries.delete(sessionId);
1108
+ await this.#disposeEntry(entry);
1109
+ }
1110
+ async dispose() {
1111
+ if (this.#disposed) return;
1112
+ this.#disposed = true;
1113
+ const entries = [...this.#entries.values()];
1114
+ this.#entries.clear();
1115
+ await Promise.allSettled(entries.map((entry) => this.#disposeEntry(entry)));
1116
+ }
1117
+ async #makeRoom() {
1118
+ if (this.#entries.size < this.#config.maxProcesses) return;
1119
+ const idle = [...this.#entries.values()].filter((entry) => entry.active === void 0 && entry.state === "idle").sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
1120
+ if (idle === void 0) throw new ClaudeProcessLimitError(this.#config.maxProcesses);
1121
+ this.#entries.delete(idle.sessionId);
1122
+ await this.#disposeEntry(idle);
1123
+ }
1124
+ async #createEntry(agent, model, thinkingMode) {
1125
+ const sessionId = agent.id;
1126
+ const cwd = agent.session.header.cwd ?? process.cwd();
1127
+ const input = new AsyncQueue();
1128
+ const lifetime = new AbortController();
1129
+ const binding = (await this.#sidecar.importLegacy(sessionId, agent.session.events)).binding;
1130
+ const permissionMode = claudePermissionMode(agent.session.events);
1131
+ const entry = {
1132
+ sessionId,
1133
+ ownerAgent: agent,
1134
+ cwd,
1135
+ model,
1136
+ thinkingMode,
1137
+ permissionMode,
1138
+ state: "starting",
1139
+ lastUsedAt: Date.now(),
1140
+ input,
1141
+ lifetime,
1142
+ claudeSessionId: binding?.claudeSessionId,
1143
+ expectedResume: binding?.claudeSessionId,
1144
+ handshakeUuid: randomUUID(),
1145
+ handshakePending: true,
1146
+ initialized: false,
1147
+ initWaiters: [],
1148
+ initTimer: void 0,
1149
+ idleTimer: void 0,
1150
+ tasks: /* @__PURE__ */ new Map(),
1151
+ taskSnapshotAt: 0,
1152
+ taskSnapshotTimer: void 0
1153
+ };
1154
+ const canUseTool = createPermissionBridge(this.#approval, () => {
1155
+ const active = entry.active;
1156
+ return active === void 0 ? void 0 : {
1157
+ agent: active.agent,
1158
+ cursor: active.cursor,
1159
+ markActivity: () => {
1160
+ active.sawActivity = true;
1161
+ },
1162
+ recordDenial: (toolUseId) => {
1163
+ active.deniedToolUseIds.add(toolUseId);
1164
+ },
1165
+ appendActivity: (activity) => this.#appendActivity(active, activity)
1166
+ };
1167
+ });
1168
+ const options = {
1169
+ pathToClaudeCodeExecutable: this.#config.executablePath,
1170
+ cwd,
1171
+ settingSources: [
1172
+ "user",
1173
+ "project",
1174
+ "local"
1175
+ ],
1176
+ systemPrompt: {
1177
+ type: "preset",
1178
+ preset: "claude_code"
1179
+ },
1180
+ tools: {
1181
+ type: "preset",
1182
+ preset: "claude_code"
1183
+ },
1184
+ includePartialMessages: true,
1185
+ permissionMode,
1186
+ allowDangerouslySkipPermissions: true,
1187
+ canUseTool,
1188
+ abortController: lifetime,
1189
+ spawnClaudeCodeProcess: createManagedClaudeSpawner(this.#runtime, this.#config.executablePath, (process) => {
1190
+ entry.process = process;
1191
+ }),
1192
+ ...binding === void 0 ? {} : { resume: binding.claudeSessionId },
1193
+ model,
1194
+ ...thinkingMode === void 0 ? {} : thinkingMode === "off" ? { thinking: { type: "disabled" } } : thinkingMode === "ultracode" ? { settings: { ultracode: true } } : { effort: thinkingMode }
1195
+ };
1196
+ entry.query = this.#queryFactory({
1197
+ prompt: input,
1198
+ options
1199
+ });
1200
+ entry.input.push(sdkUserMessage(CLAUDE_HANDSHAKE_PROMPT, entry.handshakeUuid));
1201
+ entry.pump = this.#runDetached(() => this.#pump(entry));
1202
+ return entry;
1203
+ }
1204
+ async #pump(entry) {
1205
+ try {
1206
+ for await (const sdkMessage of entry.query) for (const message of normalizeSdkMessage(sdkMessage)) await this.#handleMessage(entry, message);
1207
+ if (entry.state !== "disposed") await this.#handleDisconnect(entry, /* @__PURE__ */ new Error("Claude Code stream ended"));
1208
+ } catch (error) {
1209
+ if (entry.state !== "disposed") await this.#handleDisconnect(entry, error);
1210
+ }
1211
+ }
1212
+ async #handleMessage(entry, message) {
1213
+ if (message.kind === "init") {
1214
+ if (entry.expectedResume !== void 0 && message.sessionId !== entry.expectedResume) throw new ClaudeProtocolError(`Claude Code resumed unexpected session ${message.sessionId}; expected ${entry.expectedResume}`);
1215
+ if (message.cwd !== entry.cwd) throw new ClaudeProtocolError(`Claude Code initialized in unexpected cwd ${message.cwd}; expected ${entry.cwd}`);
1216
+ if (entry.initTimer !== void 0) {
1217
+ clearTimeout(entry.initTimer);
1218
+ entry.initTimer = void 0;
1219
+ }
1220
+ entry.initialized = true;
1221
+ entry.claudeSessionId = message.sessionId;
1222
+ entry.state = entry.active === void 0 ? "idle" : "running";
1223
+ for (const waiter of entry.initWaiters.splice(0)) waiter(void 0);
1224
+ if (entry.tasks.size > 0) {
1225
+ entry.tasks.clear();
1226
+ await this.#flushTasksSnapshot(entry);
1227
+ }
1228
+ await this.#sidecar.writeBinding(entry.sessionId, {
1229
+ claudeSessionId: message.sessionId,
1230
+ cliVersion: message.cliVersion,
1231
+ cwd: message.cwd
1232
+ });
1233
+ return;
1234
+ }
1235
+ const taskId = message.kind === "subagent" ? message.taskId : void 0;
1236
+ if (message.kind === "subagent" && taskId !== void 0) await this.#trackTask(entry, message, taskId, entry.active?.cursor.turn);
1237
+ else if (message.kind === "background-tasks") this.#trackBackgroundLevel(entry, message.tasks, entry.active?.cursor.turn);
1238
+ if (entry.handshakePending) {
1239
+ if (message.kind === "protocol-error") throw new ClaudeProtocolError(`${message.title}: ${JSON.stringify(message.detail).slice(0, 1e3)}`);
1240
+ if (message.kind === "result") entry.handshakePending = false;
1241
+ return;
1242
+ }
1243
+ const active = entry.active;
1244
+ if (active === void 0) return;
1245
+ if (message.kind === "result") {
1246
+ if (message.userMessageUuid !== void 0 && message.userMessageUuid === entry.handshakeUuid) return;
1247
+ if (entry.claudeSessionId === void 0) throw new ClaudeProtocolError("Claude Code sent a result before initialization");
1248
+ if (message.sessionId !== entry.claudeSessionId) throw new ClaudeProtocolError(`Claude Code result session ${message.sessionId} does not match ${entry.claudeSessionId}`);
1249
+ if (message.userMessageUuid !== void 0 && message.userMessageUuid !== active.promptUuid) throw new ClaudeProtocolError(`Claude Code result for user message ${message.userMessageUuid} does not match active request ${active.promptUuid}`);
1250
+ await this.#completeTurn(entry, active, message);
1251
+ return;
1252
+ }
1253
+ if (message.kind === "protocol-error") throw new ClaudeProtocolError(`${message.title}: ${JSON.stringify(message.detail).slice(0, 1e3)}`);
1254
+ active.sawActivity = true;
1255
+ switch (message.kind) {
1256
+ case "text-delta":
1257
+ if (message.parentToolUseId !== void 0) return;
1258
+ active.sawTextDelta = true;
1259
+ active.text += message.text;
1260
+ active.output.push({
1261
+ type: "text-delta",
1262
+ text: message.text
1263
+ });
1264
+ return;
1265
+ case "assistant-text":
1266
+ if (message.parentToolUseId !== void 0) return;
1267
+ if (!active.sawTextDelta) {
1268
+ active.sawTextDelta = true;
1269
+ active.text += message.text;
1270
+ active.output.push({
1271
+ type: "text-delta",
1272
+ text: message.text
1273
+ });
1274
+ }
1275
+ return;
1276
+ case "thinking":
1277
+ if (message.parentToolUseId !== void 0) return;
1278
+ if (message.phase === "updated") {
1279
+ active.thinking += message.text;
1280
+ return;
1281
+ }
1282
+ active.thinking = message.text;
1283
+ await this.#appendActivity(active, {
1284
+ kind: "thinking",
1285
+ phase: "completed",
1286
+ title: "Claude thinking",
1287
+ summary: message.text
1288
+ });
1289
+ return;
1290
+ case "tool-call":
1291
+ await this.#appendActivity(active, {
1292
+ kind: message.parentToolUseId === void 0 ? "tool-call" : "subagent",
1293
+ phase: "started",
1294
+ toolUseId: message.toolUseId,
1295
+ ...message.parentToolUseId === void 0 ? {} : { parentToolUseId: message.parentToolUseId },
1296
+ toolName: message.toolName,
1297
+ title: message.toolName,
1298
+ summary: message.parentToolUseId === void 0 ? rootCallSummary(message.toolName, message.input) : `Subagent called ${message.toolName}`,
1299
+ detail: message.input
1300
+ });
1301
+ if (message.parentToolUseId === void 0) {
1302
+ active.callNames.set(message.toolUseId, message.toolName);
1303
+ this.#ensureDynamicPresenter(active.agent, message.toolName);
1304
+ if (!TASK_TOOL_NAMES.has(message.toolName)) await this.#appendNativeToolCall(active, message);
1305
+ }
1306
+ return;
1307
+ case "tool-result":
1308
+ await this.#appendActivity(active, {
1309
+ kind: message.parentToolUseId === void 0 ? "tool-result" : "subagent",
1310
+ phase: message.isError ? "failed" : "completed",
1311
+ toolUseId: message.toolUseId,
1312
+ ...message.parentToolUseId === void 0 ? {} : { parentToolUseId: message.parentToolUseId },
1313
+ title: message.isError ? "Tool failed" : "Tool completed",
1314
+ detail: message.output,
1315
+ isError: message.isError
1316
+ });
1317
+ if (message.parentToolUseId === void 0 && !TASK_TOOL_NAMES.has(active.callNames.get(message.toolUseId) ?? "")) await this.#appendNativeToolResult(active, message);
1318
+ return;
1319
+ case "subagent":
1320
+ await this.#appendActivity(active, {
1321
+ kind: "subagent",
1322
+ phase: message.phase,
1323
+ ...message.taskId === void 0 ? {} : { taskId: message.taskId },
1324
+ title: message.title,
1325
+ summary: message.summary,
1326
+ detail: message.detail,
1327
+ isError: message.phase === "failed"
1328
+ });
1329
+ return;
1330
+ case "status":
1331
+ case "warning":
1332
+ case "unknown":
1333
+ await this.#appendActivity(active, {
1334
+ kind: message.kind === "status" ? "status" : "warning",
1335
+ phase: "updated",
1336
+ title: message.title,
1337
+ ..."summary" in message ? { summary: message.summary } : {},
1338
+ ..."detail" in message ? { detail: message.detail } : {}
1339
+ });
1340
+ return;
1341
+ case "permission-denied":
1342
+ await this.#appendActivity(active, {
1343
+ kind: "permission",
1344
+ phase: "denied",
1345
+ toolUseId: message.toolUseId,
1346
+ toolName: message.toolName,
1347
+ title: message.toolName,
1348
+ summary: message.summary
1349
+ });
1350
+ return;
1351
+ }
1352
+ }
1353
+ /** Merge one task lifecycle message into the session's task board. */
1354
+ async #trackTask(entry, message, taskId, originTurn) {
1355
+ const previous = entry.tasks.get(taskId);
1356
+ const next = {
1357
+ taskId,
1358
+ description: message.description ?? previous?.description ?? message.title,
1359
+ status: message.taskStatus ?? previous?.status ?? "running"
1360
+ };
1361
+ const resolvedOriginTurn = previous?.originTurn ?? originTurn;
1362
+ if (resolvedOriginTurn !== void 0) next.originTurn = resolvedOriginTurn;
1363
+ const subagentType = message.subagentType ?? previous?.subagentType;
1364
+ if (subagentType !== void 0) next.subagentType = subagentType;
1365
+ const taskType = message.taskType ?? previous?.taskType;
1366
+ if (taskType !== void 0) next.taskType = taskType;
1367
+ const lastToolName = message.lastToolName ?? previous?.lastToolName;
1368
+ if (lastToolName !== void 0) next.lastToolName = lastToolName;
1369
+ const summary = message.summary ?? previous?.summary;
1370
+ if (summary !== void 0) next.summary = summary;
1371
+ const usage = message.usage ?? previous?.usage;
1372
+ if (usage !== void 0) next.usage = usage;
1373
+ if (previous?.backgrounded === true) next.backgrounded = true;
1374
+ entry.tasks.set(taskId, next);
1375
+ const settled = next.status !== "running";
1376
+ await this.#scheduleTasksSnapshot(entry, settled);
1377
+ }
1378
+ /** Fold the background-task level signal into the board (REPLACE semantics
1379
+ * for the backgrounded flag: only the listed tasks are detached). */
1380
+ #trackBackgroundLevel(entry, tasks, originTurn) {
1381
+ const live = new Set(tasks.map((task) => task.taskId));
1382
+ let changed = false;
1383
+ for (const task of tasks) {
1384
+ const existing = entry.tasks.get(task.taskId);
1385
+ if (existing === void 0) {
1386
+ entry.tasks.set(task.taskId, {
1387
+ taskId: task.taskId,
1388
+ description: task.description,
1389
+ status: "running",
1390
+ ...originTurn === void 0 ? {} : { originTurn },
1391
+ ...task.taskType === void 0 ? {} : { taskType: task.taskType },
1392
+ backgrounded: true
1393
+ });
1394
+ changed = true;
1395
+ } else if (existing.backgrounded !== true || existing.status !== "running") {
1396
+ entry.tasks.set(task.taskId, {
1397
+ ...existing,
1398
+ status: "running",
1399
+ backgrounded: true
1400
+ });
1401
+ changed = true;
1402
+ }
1403
+ }
1404
+ for (const task of entry.tasks.values()) if (task.backgrounded === true && task.status === "running" && !live.has(task.taskId)) {
1405
+ entry.tasks.set(task.taskId, {
1406
+ ...task,
1407
+ status: "completed"
1408
+ });
1409
+ changed = true;
1410
+ }
1411
+ if (changed) this.#scheduleTasksSnapshot(entry, true);
1412
+ }
1413
+ /** Persist the task board. Settled transitions flush immediately; progress
1414
+ * ticks throttle to one snapshot per second to bound log volume. */
1415
+ async #scheduleTasksSnapshot(entry, immediate) {
1416
+ const THROTTLE_MS = 1e3;
1417
+ const elapsed = Date.now() - entry.taskSnapshotAt;
1418
+ if (!immediate && elapsed < THROTTLE_MS) {
1419
+ if (entry.taskSnapshotTimer === void 0) {
1420
+ entry.taskSnapshotTimer = setTimeout(() => {
1421
+ entry.taskSnapshotTimer = void 0;
1422
+ this.#flushTasksSnapshot(entry);
1423
+ }, THROTTLE_MS - elapsed);
1424
+ entry.taskSnapshotTimer.unref?.();
1425
+ }
1426
+ return;
1427
+ }
1428
+ await this.#flushTasksSnapshot(entry);
1429
+ }
1430
+ async #flushTasksSnapshot(entry) {
1431
+ if (entry.taskSnapshotTimer !== void 0) {
1432
+ clearTimeout(entry.taskSnapshotTimer);
1433
+ entry.taskSnapshotTimer = void 0;
1434
+ }
1435
+ entry.taskSnapshotAt = Date.now();
1436
+ await this.#sidecar.writeTasks(entry.sessionId, [...entry.tasks.values()]).catch(() => void 0);
1437
+ }
1438
+ async #completeTurn(entry, active, result) {
1439
+ if (entry.active !== active) return;
1440
+ if (active.signal !== void 0 && active.abortListener !== void 0) active.signal.removeEventListener("abort", active.abortListener);
1441
+ if (active.aborted) {
1442
+ await this.#appendSafely(active, {
1443
+ kind: "status",
1444
+ phase: "failed",
1445
+ title: "Claude Code turn cancelled"
1446
+ });
1447
+ entry.active = void 0;
1448
+ entry.state = "idle";
1449
+ entry.lastUsedAt = Date.now();
1450
+ this.#armIdleTimer(entry);
1451
+ return;
1452
+ }
1453
+ if (result.usage.inputTokens !== void 0 || result.usage.outputTokens !== void 0 || result.usage.cumulativeCostUsd !== void 0) {
1454
+ await this.#appendSafely(active, {
1455
+ kind: "usage",
1456
+ phase: "completed",
1457
+ title: "Claude usage",
1458
+ summary: usageSummary(result.usage),
1459
+ usage: result.usage
1460
+ });
1461
+ active.output.push({
1462
+ type: "usage",
1463
+ usage: result.usage
1464
+ });
1465
+ }
1466
+ const unmatchedDenials = (result.permissionDenials ?? []).filter((denial) => !active.deniedToolUseIds.has(denial.toolUseId));
1467
+ if (unmatchedDenials.length > 0) await this.#appendSafely(active, {
1468
+ kind: "permission",
1469
+ phase: "denied",
1470
+ title: "Claude Code auto-denied tool calls",
1471
+ summary: unmatchedDenials.map((denial) => denial.toolName).join(", ")
1472
+ });
1473
+ if (!result.success) {
1474
+ const message = result.errors?.join("\n") ?? (result.terminalReason !== void 0 ? `Claude Code failed the turn (${result.terminalReason})` : "Claude Code failed the turn");
1475
+ await this.#appendSafely(active, {
1476
+ kind: "error",
1477
+ phase: "failed",
1478
+ title: "Claude Code turn failed",
1479
+ summary: message,
1480
+ isError: true
1481
+ });
1482
+ active.output.fail(new Error(message));
1483
+ } else {
1484
+ if (!active.sawTextDelta && active.text.length === 0 && result.text !== void 0) {
1485
+ active.text = result.text;
1486
+ active.output.push({
1487
+ type: "text-delta",
1488
+ text: result.text
1489
+ });
1490
+ }
1491
+ await this.#appendSafely(active, {
1492
+ kind: "status",
1493
+ phase: "completed",
1494
+ title: "Claude Code turn completed"
1495
+ });
1496
+ active.output.push({
1497
+ type: "complete",
1498
+ text: active.text
1499
+ });
1500
+ active.output.close();
1501
+ }
1502
+ entry.active = void 0;
1503
+ entry.state = "idle";
1504
+ entry.lastUsedAt = Date.now();
1505
+ this.#armIdleTimer(entry);
1506
+ }
1507
+ async #appendActivity(active, activity) {
1508
+ const ordinal = active.cursor.nextOrdinal++;
1509
+ await this.#sidecar.appendActivity(active.agent.id, {
1510
+ ...activity,
1511
+ turn: active.cursor.turn,
1512
+ step: active.cursor.step,
1513
+ ordinal
1514
+ });
1515
+ }
1516
+ /** Persist durable activity without letting a storage failure unsettle the
1517
+ * in-memory turn or leak process ownership. Audit failure is best-effort. */
1518
+ async #appendSafely(active, activity) {
1519
+ await this.#appendActivity(active, activity).catch(() => void 0);
1520
+ }
1521
+ /** Register one presenter-only mirror for a tool name the static preset
1522
+ * registry does not cover (MCP tools, newly added built-ins). Runs in the
1523
+ * agent scope so the mirror is visible only to this preset's sessions and
1524
+ * unwinds with the agent; failure keeps the generic card, never the turn. */
1525
+ #ensureDynamicPresenter(agent, name) {
1526
+ if (CLAUDE_PRESENTER_NAMES.has(name)) return;
1527
+ let known = this.#dynamicPresenterNames.get(agent);
1528
+ if (known === void 0) {
1529
+ known = /* @__PURE__ */ new Set();
1530
+ this.#dynamicPresenterNames.set(agent, known);
1531
+ }
1532
+ if (known.has(name)) return;
1533
+ try {
1534
+ agent.ctx.tools.register(dynamicPresenterDefinition(name));
1535
+ known.add(name);
1536
+ } catch {}
1537
+ }
1538
+ /** Mirror one root Claude tool call into the durable native tool channel so
1539
+ * the host's tool presentation renders it exactly like a DSH-executed call.
1540
+ * Presentation duplication is best-effort and never unsettles the turn. */
1541
+ async #appendNativeToolCall(active, message) {
1542
+ try {
1543
+ await active.agent.session.append("tool/call", {
1544
+ turn: active.cursor.turn,
1545
+ step: active.cursor.step,
1546
+ callId: CallId(message.toolUseId),
1547
+ name: message.toolName,
1548
+ arguments: safeDetail(message.input) ?? "{}"
1549
+ });
1550
+ } catch {}
1551
+ }
1552
+ async #appendNativeToolResult(active, message) {
1553
+ const text = typeof message.output === "string" ? redactText(message.output) : safeDetail(message.output) ?? "";
1554
+ try {
1555
+ await active.agent.session.append("tool/result", {
1556
+ turn: active.cursor.turn,
1557
+ step: active.cursor.step,
1558
+ message: createToolResultMessage({
1559
+ callId: CallId(message.toolUseId),
1560
+ content: [{
1561
+ type: "text",
1562
+ text
1563
+ }],
1564
+ isError: message.isError
1565
+ })
1566
+ }, { surfaceOp: "append" });
1567
+ } catch {}
1568
+ }
1569
+ async #interrupt(entry) {
1570
+ const active = entry.active;
1571
+ if (active === void 0 || entry.state === "interrupting") return;
1572
+ entry.state = "interrupting";
1573
+ active.aborted = true;
1574
+ active.output.fail(abortFailure());
1575
+ let interruptError;
1576
+ try {
1577
+ if (((await withTimeout(entry.query.interrupt(), 5e3, "Claude Code interrupt"))?.still_queued ?? []).includes(active.promptUuid)) throw new Error(`Claude Code interrupt left submitted prompt ${active.promptUuid} queued`);
1578
+ } catch (error) {
1579
+ interruptError = error;
1580
+ }
1581
+ try {
1582
+ await this.#appendActivity(active, {
1583
+ kind: "status",
1584
+ phase: "failed",
1585
+ title: interruptError === void 0 ? "Claude Code turn cancelled" : "Claude Code cancelled; process entry reset",
1586
+ ...interruptError === void 0 ? {} : { summary: errorSummary(interruptError) }
1587
+ });
1588
+ } catch {}
1589
+ if (this.#entries.get(entry.sessionId) === entry) this.#entries.delete(entry.sessionId);
1590
+ await this.#disposeEntry(entry);
1591
+ }
1592
+ async #handleDisconnect(entry, error) {
1593
+ const active = entry.active;
1594
+ const stderr = entry.process?.stderrTail();
1595
+ if (active !== void 0) {
1596
+ if (active.signal !== void 0 && active.abortListener !== void 0) active.signal.removeEventListener("abort", active.abortListener);
1597
+ const unknown = active.sawActivity;
1598
+ entry.state = unknown ? "outcome-unknown" : "disconnected";
1599
+ const failure = unknown ? new ClaudeOutcomeUnknownError(stderr === void 0 || stderr.length === 0 ? void 0 : `Claude Code exited after activity; outcome unknown. ${stderr}`) : new Error(stderr === void 0 || stderr.length === 0 ? errorSummary(error) : stderr);
1600
+ await this.#appendSafely(active, {
1601
+ kind: "error",
1602
+ phase: "failed",
1603
+ title: unknown ? "Claude Code outcome unknown" : "Claude Code disconnected",
1604
+ summary: failure.message,
1605
+ isError: true,
1606
+ detail: error
1607
+ });
1608
+ active.output.fail(failure);
1609
+ entry.active = void 0;
1610
+ } else entry.state = "disconnected";
1611
+ const waiters = entry.initWaiters.splice(0);
1612
+ const waiterError = error instanceof Error ? error : new Error(String(error));
1613
+ for (const waiter of waiters) waiter(waiterError);
1614
+ this.#entries.delete(entry.sessionId);
1615
+ await this.#disposeEntry(entry);
1616
+ }
1617
+ #armInitializationTimer(entry) {
1618
+ const timer = setTimeout(() => {
1619
+ if (entry.state !== "starting" || entry.initialized) return;
1620
+ this.#handleDisconnect(entry, /* @__PURE__ */ new Error("Claude Code initialization timed out"));
1621
+ }, CLAUDE_INITIALIZATION_TIMEOUT_MS);
1622
+ timer.unref?.();
1623
+ entry.initTimer = timer;
1624
+ }
1625
+ #armIdleTimer(entry) {
1626
+ if (this.#config.idleTimeoutMs <= 0) return;
1627
+ const timer = setTimeout(() => {
1628
+ if (entry.active !== void 0 || entry.state !== "idle") return;
1629
+ this.#entries.delete(entry.sessionId);
1630
+ this.#disposeEntry(entry);
1631
+ }, this.#config.idleTimeoutMs);
1632
+ timer.unref?.();
1633
+ entry.idleTimer = timer;
1634
+ }
1635
+ async #disposeEntry(entry) {
1636
+ if (entry.state === "disposed") return;
1637
+ if (entry.idleTimer !== void 0) clearTimeout(entry.idleTimer);
1638
+ if (entry.initTimer !== void 0) clearTimeout(entry.initTimer);
1639
+ entry.state = "disposed";
1640
+ entry.input.discard(abortFailure());
1641
+ entry.query.close();
1642
+ entry.lifetime.abort();
1643
+ if (entry.active !== void 0) entry.active.output.fail(abortFailure());
1644
+ entry.process?.kill("SIGTERM");
1645
+ if (entry.process !== void 0) try {
1646
+ await entry.process.handle.waitForExit(AbortSignal.timeout(5e3));
1647
+ } catch {}
1648
+ }
1649
+ };
1650
+ //#endregion
1651
+ //#region src/adapter.ts
1652
+ const MODELS = [
1653
+ {
1654
+ id: "default",
1655
+ name: "Default (recommended)",
1656
+ description: "Use Claude Code’s recommended default model."
1657
+ },
1658
+ {
1659
+ id: "opus[1m]",
1660
+ name: "Opus (1M context)",
1661
+ description: "Use Opus with a 1M-token context window.",
1662
+ contextWindow: 1e6
1663
+ },
1664
+ {
1665
+ id: "fable",
1666
+ name: "Fable",
1667
+ description: "Use Fable, Claude Code’s most capable coding model."
1668
+ },
1669
+ {
1670
+ id: "sonnet",
1671
+ name: "Sonnet",
1672
+ description: "Use Sonnet for efficient routine coding work."
1673
+ },
1674
+ {
1675
+ id: "haiku",
1676
+ name: "Haiku",
1677
+ description: "Use Haiku for fast, lightweight tasks."
1678
+ }
1679
+ ];
1680
+ const THINKING_MODES = [
1681
+ {
1682
+ id: "off",
1683
+ name: "Off",
1684
+ description: "No extended thinking."
1685
+ },
1686
+ {
1687
+ id: "low",
1688
+ name: "Low",
1689
+ description: "Minimal thinking, fastest responses."
1690
+ },
1691
+ {
1692
+ id: "medium",
1693
+ name: "Medium",
1694
+ description: "Moderate thinking."
1695
+ },
1696
+ {
1697
+ id: "high",
1698
+ name: "High",
1699
+ description: "Deep reasoning (Claude Code default)."
1700
+ },
1701
+ {
1702
+ id: "xhigh",
1703
+ name: "Extra High",
1704
+ description: "Deeper than high; unsupported models silently downgrade to high."
1705
+ },
1706
+ {
1707
+ id: "max",
1708
+ name: "Max",
1709
+ description: "Maximum effort; unsupported models silently downgrade."
1710
+ },
1711
+ {
1712
+ id: "ultracode",
1713
+ name: "Ultracode",
1714
+ description: "Extra-high effort plus standing dynamic-workflow orchestration; requires an xhigh-capable model."
1715
+ }
1716
+ ];
1717
+ function thinkingModeFor(effort) {
1718
+ if (effort === void 0) return void 0;
1719
+ if (THINKING_MODES.some((mode) => mode.id === effort)) return effort;
1720
+ throw new Error(`dsh-claude: unsupported reasoning effort ${JSON.stringify(effort)}`);
1721
+ }
1722
+ const NO_RETRY_POLICY = Object.freeze({
1723
+ mode: "normal",
1724
+ maxRetries: 0,
1725
+ retryableCodes: Object.freeze([]),
1726
+ initialDelayMs: 500,
1727
+ maxDelayMs: 1e4,
1728
+ jitterRatio: .1
1729
+ });
1730
+ function extractDirectUserText(messages) {
1731
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
1732
+ const message = messages[index];
1733
+ if (message?.role !== "user" || message.source.kind !== "user") continue;
1734
+ const text = message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
1735
+ if (text.length > 0) return text;
1736
+ if (message.content.some((block) => block.type === "image")) throw new Error("dsh-claude: image-only prompts are not supported in v0.1; include a text prompt");
1737
+ }
1738
+ throw new Error("dsh-claude: no direct human text was present in this model step");
1739
+ }
1740
+ function tokenUsage(usage) {
1741
+ const normalized = {
1742
+ inputTokens: usage.inputTokens ?? 0,
1743
+ outputTokens: usage.outputTokens ?? 0
1744
+ };
1745
+ if (usage.cacheReadTokens !== void 0) normalized.cacheReadTokens = usage.cacheReadTokens;
1746
+ if (usage.cacheCreationTokens !== void 0) normalized.cacheWriteTokens = usage.cacheCreationTokens;
1747
+ return normalized;
1748
+ }
1749
+ function resolveAgent(agents, options) {
1750
+ const initiator = agents.currentInitiator();
1751
+ if (initiator !== void 0) return initiator;
1752
+ if (options.sessionId !== void 0) {
1753
+ const agent = agents.get(options.sessionId);
1754
+ if (agent !== void 0) return agent;
1755
+ }
1756
+ throw new Error("dsh-claude: the model request has no live owning DSH agent");
1757
+ }
1758
+ var ClaudeCodeAdapter = class extends LlmAdapter {
1759
+ #supervisor;
1760
+ #agents;
1761
+ #presetIdFor;
1762
+ constructor(supervisor, agents, presetIdFor) {
1763
+ super();
1764
+ this.#supervisor = supervisor;
1765
+ this.#agents = agents;
1766
+ this.#presetIdFor = presetIdFor;
1767
+ }
1768
+ providerInfo(provider) {
1769
+ return {
1770
+ id: provider,
1771
+ name: "Claude Code"
1772
+ };
1773
+ }
1774
+ providerRetryPolicy() {
1775
+ return NO_RETRY_POLICY;
1776
+ }
1777
+ async listModels() {
1778
+ return MODELS.map((model) => ({
1779
+ provider: CLAUDE_CODE_PROVIDER,
1780
+ id: model.id,
1781
+ name: model.name,
1782
+ description: model.description,
1783
+ inputModalities: ["text"]
1784
+ }));
1785
+ }
1786
+ async resolveModel(provider, model) {
1787
+ const known = MODELS.find((item) => item.id === model);
1788
+ const contextWindow = this.#supervisor.contextWindow(model) ?? (known !== void 0 && "contextWindow" in known ? known.contextWindow : void 0);
1789
+ return {
1790
+ provider,
1791
+ id: model,
1792
+ name: known?.name ?? `Claude Code ${model}`,
1793
+ ...known === void 0 ? {} : { description: known.description },
1794
+ ...contextWindow === void 0 ? {} : { context: { contextWindow } },
1795
+ inputModalities: ["text"],
1796
+ reasoning: { efforts: THINKING_MODES.map((mode) => ({
1797
+ id: ReasoningEffortId(mode.id),
1798
+ name: mode.name,
1799
+ description: mode.description
1800
+ })) }
1801
+ };
1802
+ }
1803
+ async *stream(options) {
1804
+ if (options.purpose !== void 0) throw new Error(`dsh-claude: auxiliary ${options.purpose} calls are not routed into the Claude session`);
1805
+ const agent = resolveAgent(this.#agents, options);
1806
+ if (!isClaudePresetId(this.#presetIdFor(agent))) throw new Error(`dsh-claude: provider ${CLAUDE_CODE_PROVIDER} is available only to the ${CLAUDE_CODE_PRESET_ID} preset`);
1807
+ const thinkingMode = thinkingModeFor(options.reasoningEffort);
1808
+ const prompt = extractDirectUserText(options.messages);
1809
+ const events = await this.#supervisor.runTurn({
1810
+ agent,
1811
+ prompt,
1812
+ model: options.model,
1813
+ ...thinkingMode === void 0 ? {} : { thinkingMode },
1814
+ ...options.signal === void 0 ? {} : { signal: options.signal }
1815
+ });
1816
+ let text = "";
1817
+ let pendingUsage;
1818
+ let completed = false;
1819
+ try {
1820
+ for await (const event of events) if (event.type === "text-delta") text += event.text;
1821
+ else if (event.type === "usage") pendingUsage = tokenUsage(event.usage);
1822
+ else {
1823
+ completed = true;
1824
+ if (text.length > 0) {
1825
+ yield {
1826
+ type: "block-start",
1827
+ index: 0,
1828
+ blockType: "text"
1829
+ };
1830
+ yield {
1831
+ type: "text-delta",
1832
+ index: 0,
1833
+ text
1834
+ };
1835
+ yield {
1836
+ type: "block-end",
1837
+ index: 0,
1838
+ block: {
1839
+ type: "text",
1840
+ text
1841
+ }
1842
+ };
1843
+ }
1844
+ if (pendingUsage !== void 0) yield {
1845
+ type: "usage",
1846
+ usage: pendingUsage
1847
+ };
1848
+ yield {
1849
+ type: "finish",
1850
+ reason: { kind: "stop" }
1851
+ };
1852
+ }
1853
+ } catch (error) {
1854
+ if (error.name === "AbortError") {
1855
+ completed = true;
1856
+ if (text.length > 0) {
1857
+ yield {
1858
+ type: "block-start",
1859
+ index: 0,
1860
+ blockType: "text"
1861
+ };
1862
+ yield {
1863
+ type: "text-delta",
1864
+ index: 0,
1865
+ text
1866
+ };
1867
+ yield {
1868
+ type: "block-end",
1869
+ index: 0,
1870
+ block: {
1871
+ type: "text",
1872
+ text
1873
+ }
1874
+ };
1875
+ }
1876
+ yield {
1877
+ type: "finish",
1878
+ reason: {
1879
+ kind: "aborted",
1880
+ failure: {
1881
+ code: "aborted",
1882
+ message: error instanceof Error ? error.message : "Claude Code turn aborted"
1883
+ }
1884
+ }
1885
+ };
1886
+ return;
1887
+ }
1888
+ throw error;
1889
+ }
1890
+ if (!completed) throw new Error("dsh-claude: Claude turn stream ended without a result");
1891
+ }
1892
+ };
1893
+ function createClaudeCodeAdapter(supervisor, agents, presetIdFor) {
1894
+ return new ClaudeCodeAdapter(supervisor, agents, presetIdFor);
1895
+ }
1896
+ //#endregion
1897
+ //#region src/http.ts
1898
+ /** Accept only loopback, same-origin browser requests to plugin-private routes. */
1899
+ function trustedRequest(req) {
1900
+ const remote = req.socket.remoteAddress;
1901
+ if (remote !== "127.0.0.1" && remote !== "::1" && remote !== "::ffff:127.0.0.1") return false;
1902
+ if (req.headers["sec-fetch-site"] === "cross-site") return false;
1903
+ const host = req.headers.host;
1904
+ if (host === void 0) return false;
1905
+ const authority = /^(?:127\.0\.0\.1|\[?::1\]?|localhost)(?::\d+)?$/i;
1906
+ if (!authority.test(host)) return false;
1907
+ const origin = req.headers.origin;
1908
+ if (origin === void 0) return true;
1909
+ try {
1910
+ const originUrl = new URL(origin);
1911
+ return originUrl.host === host && authority.test(originUrl.host);
1912
+ } catch {
1913
+ return false;
1914
+ }
1915
+ }
1916
+ /** Send a non-cacheable JSON response with MIME sniffing disabled. */
1917
+ function json(res, status, value) {
1918
+ res.writeHead(status, {
1919
+ "content-type": "application/json; charset=utf-8",
1920
+ "cache-control": "no-store",
1921
+ "x-content-type-options": "nosniff"
1922
+ });
1923
+ res.end(JSON.stringify(value));
1924
+ }
1925
+ //#endregion
1926
+ //#region src/doctor-routes.ts
1927
+ const CLAUDE_DOCTOR_PROBE_TIMEOUT_MS = 15e3;
1928
+ const claudeBridgeDiagnostics = /* @__PURE__ */ new WeakMap();
1929
+ function safeMessage(error) {
1930
+ return redactText(error instanceof Error ? error.message : String(error), 1e3);
1931
+ }
1932
+ /** Live command-bridge diagnostics: which agents exist, their presets, and how
1933
+ * many slash commands each agent's registry layer resolves. */
1934
+ function commandDiagnostics(ctx) {
1935
+ try {
1936
+ const agents = ctx.agents.list();
1937
+ return {
1938
+ total: agents.length,
1939
+ agents: agents.map((agent) => {
1940
+ const info = { id: String(agent.id) };
1941
+ try {
1942
+ const preset = ctx.agentPresets.composedPreset(agent.ctx);
1943
+ if (preset !== void 0) info.preset = preset;
1944
+ } catch (error) {
1945
+ info.error = safeMessage(error);
1946
+ }
1947
+ try {
1948
+ const list = ctx.commands.list(agent);
1949
+ info.commandCount = list.length;
1950
+ info.sample = list.slice(0, 10).map((command) => command.name);
1951
+ } catch (error) {
1952
+ info.error = info.error === void 0 ? safeMessage(error) : `${info.error}; ${safeMessage(error)}`;
1953
+ }
1954
+ const bridge = claudeBridgeDiagnostics.get(agent);
1955
+ if (bridge !== void 0) info.bridge = bridge;
1956
+ return info;
1957
+ })
1958
+ };
1959
+ } catch (error) {
1960
+ return { error: safeMessage(error) };
1961
+ }
1962
+ }
1963
+ function registerClaudeDoctorRoutes(ctx, runtime, supervisor, config, resolutionError) {
1964
+ ctx.effect(() => ctx.webServer.register({
1965
+ kind: "exact",
1966
+ path: CLAUDE_DOCTOR_PATH,
1967
+ handler: async (req, res) => {
1968
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
1969
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
1970
+ try {
1971
+ if (resolutionError !== void 0) return json(res, 200, {
1972
+ executable: {
1973
+ status: "missing",
1974
+ searched: config.executablePath.length > 0 ? [config.executablePath] : [
1975
+ "claude",
1976
+ "~/.local/bin/claude",
1977
+ "/opt/homebrew/bin/claude",
1978
+ "/usr/local/bin/claude"
1979
+ ]
1980
+ },
1981
+ version: { status: "not-run" },
1982
+ authentication: { status: "not-run" },
1983
+ handshake: "not-run",
1984
+ message: safeMessage(resolutionError),
1985
+ limits: {
1986
+ idleTimeoutMs: config.idleTimeoutMs,
1987
+ maxProcesses: config.maxProcesses
1988
+ },
1989
+ processes: {
1990
+ count: 0,
1991
+ active: 0
1992
+ }
1993
+ });
1994
+ const report = await runClaudeDoctor(runtime, {
1995
+ configuredPath: config.executablePath,
1996
+ cwd: process.cwd(),
1997
+ signal: AbortSignal.timeout(CLAUDE_DOCTOR_PROBE_TIMEOUT_MS)
1998
+ });
1999
+ const processes = supervisor.snapshots();
2000
+ if (processes.some((process) => process.claudeSessionId !== void 0)) report.handshake = "ok";
2001
+ json(res, 200, {
2002
+ ...report,
2003
+ limits: {
2004
+ idleTimeoutMs: config.idleTimeoutMs,
2005
+ maxProcesses: config.maxProcesses
2006
+ },
2007
+ processes: {
2008
+ count: processes.length,
2009
+ active: processes.filter((process) => process.state === "running" || process.state === "starting").length
2010
+ },
2011
+ commandBridge: commandDiagnostics(ctx)
2012
+ });
2013
+ } catch (error) {
2014
+ json(res, 500, { error: safeMessage(error) });
2015
+ }
2016
+ }
2017
+ }), "dsh-claude: Doctor route");
2018
+ }
2019
+ //#endregion
2020
+ //#region src/projection-routes.ts
2021
+ const MAX_SESSION_ID_CHARS = 1024;
2022
+ function sessionIdFromUrl(rawUrl) {
2023
+ try {
2024
+ const pathname = new URL(rawUrl ?? "/", "http://localhost").pathname;
2025
+ const prefix = `${CLAUDE_PROJECTION_PATH}/`;
2026
+ if (!pathname.startsWith(prefix)) return void 0;
2027
+ const encoded = pathname.slice(prefix.length);
2028
+ if (encoded.length === 0 || encoded.includes("/")) return void 0;
2029
+ const sessionId = decodeURIComponent(encoded);
2030
+ if (sessionId.length === 0 || sessionId.length > MAX_SESSION_ID_CHARS) return void 0;
2031
+ return sessionId;
2032
+ } catch {
2033
+ return;
2034
+ }
2035
+ }
2036
+ /** Register the browser-readable, credential-free sidecar projection endpoint. */
2037
+ function registerClaudeProjectionRoute(ctx, sidecar, ownsSession) {
2038
+ ctx.effect(() => ctx.webServer.register({
2039
+ kind: "prefix",
2040
+ path: CLAUDE_PROJECTION_PATH,
2041
+ handler: async (req, res) => {
2042
+ if (req.method !== "GET") return json(res, 405, { error: "method not allowed" });
2043
+ if (!trustedRequest(req)) return json(res, 403, { error: "forbidden" });
2044
+ const sessionId = sessionIdFromUrl(req.url);
2045
+ if (sessionId === void 0) return json(res, 400, { error: "invalid session id" });
2046
+ try {
2047
+ const projection = await sidecar.read(sessionId);
2048
+ return json(res, 200, {
2049
+ schemaVersion: projection.schemaVersion,
2050
+ revision: projection.revision,
2051
+ owned: ownsSession(sessionId),
2052
+ activities: projection.activities,
2053
+ ...projection.contextUsage === void 0 ? {} : { contextUsage: projection.contextUsage },
2054
+ ...projection.tasks === void 0 ? {} : { tasks: projection.tasks }
2055
+ });
2056
+ } catch {
2057
+ return json(res, 500, { error: "projection unavailable" });
2058
+ }
2059
+ }
2060
+ }), "dsh-claude: sidecar projection route");
2061
+ }
2062
+ //#endregion
2063
+ //#region src/index.ts
2064
+ const name = "llm-claude";
2065
+ const inject = [
2066
+ "llm",
2067
+ "agents",
2068
+ "agentPresets",
2069
+ "commands",
2070
+ "subprocess",
2071
+ "approval"
2072
+ ];
2073
+ const Config = z.object({
2074
+ executablePath: z.string().default(""),
2075
+ model: z.string().default("default"),
2076
+ idleTimeoutMs: z.number().min(1e3).max(2147483647).default(18e5),
2077
+ maxProcesses: z.number().step(1).min(1).default(4)
2078
+ });
2079
+ const CLAUDE_SCOPE_UNAVAILABLE_MESSAGE = "agent command scope unavailable (preset route not mounted?)";
2080
+ const CATALOG_RETRY_MS = 5e3;
2081
+ const SCOPE_RETRY_MS = 500;
2082
+ const MAX_CATALOG_RETRIES = 3;
2083
+ const MAX_SCOPE_RETRIES = 24;
2084
+ function mountClaudeMetadata(ctx, supervisor, agent, model, sidecar, resolveCommands = () => ctx.agentPresets.serviceFor(agent, CLAUDE_COMMANDS_SERVICE)) {
2085
+ if (!isClaudePresetId(ctx.agentPresets.composedPreset(agent.ctx))) return void 0;
2086
+ let stopped = false;
2087
+ let pending = Promise.resolve();
2088
+ let commandScope;
2089
+ const scopedCommands = () => {
2090
+ const scoped = commandScope ?? resolveCommands();
2091
+ if (scoped === void 0) throw new Error(CLAUDE_SCOPE_UNAVAILABLE_MESSAGE);
2092
+ commandScope = scoped;
2093
+ return scoped;
2094
+ };
2095
+ const bridge = new ClaudeCommandBridge({
2096
+ list: () => scopedCommands().list(agent),
2097
+ register: (definition) => scopedCommands().register(definition),
2098
+ forward: (line) => {
2099
+ agent.followup(createUserMessage({
2100
+ content: [{
2101
+ type: "text",
2102
+ text: line
2103
+ }],
2104
+ source: { kind: "user" }
2105
+ }));
2106
+ }
2107
+ });
2108
+ const warn = (area, error) => {
2109
+ ctx.logger.warn(`dsh-claude: ${area} refresh failed for ${String(agent.id)}: ${error instanceof Error ? error.message : String(error)}`);
2110
+ };
2111
+ const isScopeUnavailable = (error) => {
2112
+ if (error instanceof Error) return error.message === CLAUDE_SCOPE_UNAVAILABLE_MESSAGE;
2113
+ return String(error) === CLAUDE_SCOPE_UNAVAILABLE_MESSAGE;
2114
+ };
2115
+ const diagnostic = claudeBridgeDiagnostics.get(agent) ?? { attempts: 0 };
2116
+ claudeBridgeDiagnostics.set(agent, diagnostic);
2117
+ let catalogRetries = 0;
2118
+ let scopeRetries = 0;
2119
+ let retryTimer;
2120
+ const scheduleRetry = (area, attempt) => {
2121
+ if (retryTimer !== void 0) clearTimeout(retryTimer);
2122
+ const delay = area === "command catalog" ? CATALOG_RETRY_MS * attempt : Math.min(SCOPE_RETRY_MS * 2 ** attempt, 5e3);
2123
+ retryTimer = setTimeout(() => {
2124
+ if (!stopped) refresh();
2125
+ }, delay);
2126
+ retryTimer.unref?.();
2127
+ };
2128
+ const refresh = () => {
2129
+ pending = pending.then(async () => {
2130
+ if (stopped) return;
2131
+ diagnostic.attempts += 1;
2132
+ let catalog;
2133
+ try {
2134
+ catalog = await supervisor.supportedCommands(agent, model);
2135
+ catalogRetries = 0;
2136
+ scopeRetries = 0;
2137
+ diagnostic.lastCatalog = catalog.length;
2138
+ delete diagnostic.lastError;
2139
+ } catch (error) {
2140
+ diagnostic.lastError = error instanceof Error ? error.message : String(error);
2141
+ warn("command catalog", error);
2142
+ if (!stopped && catalogRetries < MAX_CATALOG_RETRIES) {
2143
+ catalogRetries += 1;
2144
+ scheduleRetry("command catalog", catalogRetries);
2145
+ }
2146
+ }
2147
+ if (stopped || catalog === void 0) return;
2148
+ try {
2149
+ diagnostic.registered = bridge.refresh(catalog).map((view) => view.publicName);
2150
+ if (!stopped) scopeRetries = 0;
2151
+ } catch (error) {
2152
+ diagnostic.lastError = error instanceof Error ? error.message : String(error);
2153
+ warn("command catalog", error);
2154
+ if (!stopped && isScopeUnavailable(error) && scopeRetries < MAX_SCOPE_RETRIES) {
2155
+ scopeRetries += 1;
2156
+ scheduleRetry("command scope", scopeRetries);
2157
+ }
2158
+ }
2159
+ if (stopped) return;
2160
+ try {
2161
+ const usage = await supervisor.contextUsage(agent, model);
2162
+ if (!stopped) await sidecar.writeContextUsage(agent.id, usage);
2163
+ } catch (error) {
2164
+ warn("context usage", error);
2165
+ }
2166
+ });
2167
+ };
2168
+ return agent.ctx.effect(() => {
2169
+ const stopStatus = agent.ctx.on("agent/status", ({ status }) => {
2170
+ if (status === "idle") refresh();
2171
+ });
2172
+ refresh();
2173
+ return async () => {
2174
+ stopped = true;
2175
+ if (retryTimer !== void 0) clearTimeout(retryTimer);
2176
+ stopStatus();
2177
+ await pending;
2178
+ bridge.dispose();
2179
+ };
2180
+ }, "dsh-claude: agent metadata bridge");
2181
+ }
2182
+ async function apply(ctx, config) {
2183
+ await ensureManagedPreset();
2184
+ const supervisorConfig = {
2185
+ executablePath: "",
2186
+ defaultModel: config.model ?? "default",
2187
+ idleTimeoutMs: config.idleTimeoutMs ?? 18e5,
2188
+ maxProcesses: config.maxProcesses ?? 4
2189
+ };
2190
+ const sidecar = new ClaudeSidecarRepository();
2191
+ const supervisor = new ClaudeSupervisor({
2192
+ runtime: ctx.subprocess,
2193
+ approval: ctx.approval,
2194
+ config: supervisorConfig,
2195
+ runDetached: (operation) => ctx.agents.withoutInitiator(operation),
2196
+ sidecar
2197
+ });
2198
+ let resolutionError;
2199
+ try {
2200
+ supervisorConfig.executablePath = (await resolveClaudeExecutable(ctx.subprocess, config.executablePath === void 0 || config.executablePath.length === 0 ? void 0 : config.executablePath)).path;
2201
+ ctx.llm.registerAdapter([...CLAUDE_CODE_PROVIDER_IDS], createClaudeCodeAdapter(supervisor, ctx.agents, (agent) => ctx.agentPresets.composedPreset(agent.ctx)));
2202
+ ctx.effect(() => {
2203
+ const mounted = /* @__PURE__ */ new Map();
2204
+ const pending = /* @__PURE__ */ new Set();
2205
+ const MOUNT_RETRY_MS = 200;
2206
+ const MOUNT_RETRY_LIMIT = 50;
2207
+ const mount = (agent) => {
2208
+ if (mounted.has(agent)) return;
2209
+ const dispose = mountClaudeMetadata(ctx, supervisor, agent, supervisorConfig.defaultModel, sidecar);
2210
+ if (dispose !== void 0) mounted.set(agent, dispose);
2211
+ pending.delete(agent);
2212
+ };
2213
+ const mountWhenPresetSettles = (agent) => {
2214
+ if (mounted.has(agent) || pending.has(agent)) return;
2215
+ if (ctx.agentPresets.composedPreset(agent.ctx) !== void 0) {
2216
+ mount(agent);
2217
+ return;
2218
+ }
2219
+ pending.add(agent);
2220
+ let attempts = 0;
2221
+ const retry = () => {
2222
+ if (mounted.has(agent) || !pending.has(agent)) return;
2223
+ if (ctx.agentPresets.composedPreset(agent.ctx) !== void 0) {
2224
+ mount(agent);
2225
+ return;
2226
+ }
2227
+ attempts += 1;
2228
+ if (attempts >= MOUNT_RETRY_LIMIT) {
2229
+ pending.delete(agent);
2230
+ return;
2231
+ }
2232
+ setTimeout(retry, MOUNT_RETRY_MS).unref?.();
2233
+ };
2234
+ setTimeout(retry, MOUNT_RETRY_MS).unref?.();
2235
+ };
2236
+ const stopCreated = ctx.on("agent/created", ({ agent }) => {
2237
+ mountWhenPresetSettles(agent);
2238
+ });
2239
+ const onPresetSelected = ctx.on;
2240
+ const stopSelected = onPresetSelected("agent-preset/selected", (sessionId, preset) => {
2241
+ if (!isClaudePresetId(preset)) return;
2242
+ const agent = ctx.agents.get(sessionId);
2243
+ if (agent !== void 0) mountWhenPresetSettles(agent);
2244
+ });
2245
+ for (const agent of ctx.agents.list()) mountWhenPresetSettles(agent);
2246
+ return async () => {
2247
+ stopCreated();
2248
+ stopSelected();
2249
+ pending.clear();
2250
+ await Promise.allSettled([...mounted.values()].map((dispose) => dispose()));
2251
+ mounted.clear();
2252
+ };
2253
+ }, "dsh-claude: metadata bridges");
2254
+ } catch (error) {
2255
+ resolutionError = error;
2256
+ }
2257
+ ctx.on("agent/disposed", async ({ agent }) => {
2258
+ await supervisor.disposeSession(agent.id);
2259
+ });
2260
+ ctx.effect(() => () => supervisor.dispose(), "dsh-claude: process supervisor");
2261
+ ctx.inject(["webServer"], (webCtx) => {
2262
+ registerClaudeDoctorRoutes(webCtx, webCtx.subprocess, supervisor, supervisorConfig, resolutionError);
2263
+ registerClaudeProjectionRoute(webCtx, sidecar, (sessionId) => {
2264
+ const agent = webCtx.agents.get(sessionId);
2265
+ return agent !== void 0 && isClaudePresetId(webCtx.agentPresets.composedPreset(agent.ctx));
2266
+ });
2267
+ });
2268
+ }
2269
+ //#endregion
2270
+ export { Config, apply, inject, mountClaudeMetadata, name };
2271
+
2272
+ //# sourceMappingURL=index.mjs.map