@co0ontty/wand 4.46.0 → 4.47.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.
Files changed (34) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/distribution-manager.d.ts +17 -0
  3. package/dist/distribution-manager.js +80 -17
  4. package/dist/server-update-routes.d.ts +5 -0
  5. package/dist/server-update-routes.js +15 -0
  6. package/dist/server.js +8 -2
  7. package/dist/structured-claude-adapter.d.ts +3 -1
  8. package/dist/structured-claude-adapter.js +48 -93
  9. package/dist/structured-codex-adapter.d.ts +5 -0
  10. package/dist/structured-codex-adapter.js +40 -78
  11. package/dist/structured-exec-host.d.ts +76 -0
  12. package/dist/structured-exec-host.js +117 -0
  13. package/dist/structured-exec-pump.d.ts +37 -0
  14. package/dist/structured-exec-pump.js +127 -0
  15. package/dist/structured-grok-adapter.d.ts +3 -1
  16. package/dist/structured-grok-adapter.js +28 -67
  17. package/dist/structured-opencode-adapter.d.ts +3 -1
  18. package/dist/structured-opencode-adapter.js +37 -79
  19. package/dist/structured-pi-adapter.d.ts +3 -1
  20. package/dist/structured-pi-adapter.js +27 -39
  21. package/dist/structured-qoder-adapter.d.ts +3 -1
  22. package/dist/structured-qoder-adapter.js +49 -87
  23. package/dist/structured-session-manager.d.ts +9 -1
  24. package/dist/structured-session-manager.js +399 -12
  25. package/dist/terminal-daemon-client.d.ts +15 -1
  26. package/dist/terminal-daemon-client.js +261 -0
  27. package/dist/terminal-daemon-protocol.d.ts +10 -3
  28. package/dist/terminal-daemon-protocol.js +1 -1
  29. package/dist/terminal-daemon-server.js +178 -0
  30. package/dist/web-ui/content/scripts.js +57 -57
  31. package/dist/web-ui/content/styles.css +1 -1
  32. package/dist/web-ui/embedded-assets.d.ts +1 -1
  33. package/dist/web-ui/embedded-assets.js +3 -3
  34. package/package.json +1 -1
@@ -9,13 +9,15 @@ import { SessionTopicCoordinator } from "./session-topic.js";
9
9
  import { resolveSessionCwd } from "./session-cwd.js";
10
10
  import { resolveSystemAiContext } from "./session-ai-context.js";
11
11
  import { CodexRunner } from "./structured-codex-adapter.js";
12
+ import { CodexProtocolReducer } from "./structured-codex-protocol.js";
12
13
  import { normalizeStructuredToolResultContent } from "./structured-content.js";
13
14
  import { buildAppendSystemPromptParts, buildClaudeSdkThinking, ClaudeCliRunner, derivePermissionPolicy, } from "./structured-claude-adapter.js";
14
- import { captureTaskMeta, extractClaudeAssistantMessage, extractClaudeModelName, normalizeClaudeToolInput, stampParentTaskResults, stampSelfTask, tagSubagentBlocks, } from "./structured-claude-protocol.js";
15
- import { OpenCodeRunner } from "./structured-opencode-adapter.js";
16
- import { GrokRunner } from "./structured-grok-adapter.js";
15
+ import { captureTaskMeta, extractClaudeAssistantMessage, extractClaudeModelName, normalizeClaudeToolInput, stampParentTaskResults, stampSelfTask, tagSubagentBlocks, ClaudeCliProtocolReducer, } from "./structured-claude-protocol.js";
16
+ import { OpenCodeRunner, applyOpenCodeEvent } from "./structured-opencode-adapter.js";
17
+ import { GrokRunner, applyGrokEvent } from "./structured-grok-adapter.js";
17
18
  import { QoderRunner } from "./structured-qoder-adapter.js";
18
- import { PiRunner } from "./structured-pi-adapter.js";
19
+ import { PiRunner, applyPiEvent } from "./structured-pi-adapter.js";
20
+ import { structuredRunId, } from "./structured-exec-host.js";
19
21
  import { defaultStructuredRunner, defaultStructuredState, isStructuredRunnerForProvider, normalizeThinkingEffort, resolveStructuredRunner, } from "./structured-provider-common.js";
20
22
  import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
21
23
  /** The runner already persisted/emitted its detailed terminal snapshot. */
@@ -25,6 +27,122 @@ class PersistedStructuredRunnerError extends Error {
25
27
  this.name = "PersistedStructuredRunnerError";
26
28
  }
27
29
  }
30
+ function buildReplayProcessor(session) {
31
+ const runner = session.runner;
32
+ if (runner === "codex-cli-exec") {
33
+ const reducer = new CodexProtocolReducer(session);
34
+ return {
35
+ state: reducer.state,
36
+ stderr: "",
37
+ get primaryError() { return reducer.primaryError; },
38
+ get errors() { return reducer.errors; },
39
+ feed: (line) => {
40
+ const trimmed = line.trim();
41
+ if (!trimmed)
42
+ return false;
43
+ let event;
44
+ try {
45
+ event = JSON.parse(trimmed);
46
+ }
47
+ catch {
48
+ return false;
49
+ }
50
+ return reducer.apply(event);
51
+ },
52
+ };
53
+ }
54
+ if (runner === "claude-cli-print" || runner === "qoder-cli-print") {
55
+ const reducer = new ClaudeCliProtocolReducer(session);
56
+ const managed = session.mode === "managed";
57
+ const processor = {
58
+ state: reducer.state,
59
+ stderr: "",
60
+ primaryError: null,
61
+ stdoutTail: "",
62
+ get askUserQuestionDetected() { return reducer.askUserQuestionDetected; },
63
+ get stopReason() { return reducer.askUserQuestionDetected ? "ask-user-question" : undefined; },
64
+ feed: (line) => {
65
+ const trimmed = line.trim();
66
+ if (!trimmed)
67
+ return false;
68
+ processor.stdoutTail = trimmed.slice(-1024);
69
+ let event;
70
+ try {
71
+ event = JSON.parse(trimmed);
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ if (runner === "qoder-cli-print" && event && typeof event === "object" && !Array.isArray(event)) {
77
+ const record = event;
78
+ if (record.type === "result" && record.subtype !== "success") {
79
+ const errors = Array.isArray(record.errors)
80
+ ? record.errors.filter((item) => typeof item === "string")
81
+ : [];
82
+ processor.primaryError = errors.join("\n") || "Qoder CLI execution failed";
83
+ }
84
+ }
85
+ return reducer.apply(event, managed);
86
+ },
87
+ };
88
+ return processor;
89
+ }
90
+ // grok-cli-headless / opencode-cli-run / pi-cli-json share the same shape.
91
+ const state = {
92
+ blocks: [],
93
+ result: "",
94
+ sessionId: session.claudeSessionId,
95
+ model: session.selectedModel ?? session.structuredState?.model,
96
+ ...(runner === "opencode-cli-run" ? { usage: undefined } : {}),
97
+ };
98
+ const processor = {
99
+ state,
100
+ stderr: "",
101
+ primaryError: null,
102
+ feed: (line) => {
103
+ const trimmed = line.trim();
104
+ if (!trimmed)
105
+ return false;
106
+ let event;
107
+ try {
108
+ event = JSON.parse(trimmed);
109
+ }
110
+ catch {
111
+ return false;
112
+ }
113
+ const error = runner === "grok-cli-headless"
114
+ ? applyGrokEvent(state, event)
115
+ : runner === "opencode-cli-run"
116
+ ? applyOpenCodeEvent(state, event)
117
+ : applyPiEvent(state, event);
118
+ if (error)
119
+ processor.primaryError = error;
120
+ return true;
121
+ },
122
+ };
123
+ return processor;
124
+ }
125
+ function recoveredCommandLabel(runner) {
126
+ switch (runner) {
127
+ case "codex-cli-exec": return "codex exec";
128
+ case "opencode-cli-run": return "opencode run";
129
+ case "grok-cli-headless": return "grok -p --output-format streaming-json";
130
+ case "qoder-cli-print": return "qodercli -p --output-format stream-json";
131
+ case "pi-cli-json": return "pi --mode json";
132
+ default: return "claude -p";
133
+ }
134
+ }
135
+ function numericToSignal(signal) {
136
+ if (signal === null || signal === 0)
137
+ return null;
138
+ const names = {
139
+ 1: "SIGHUP", 2: "SIGINT", 3: "SIGQUIT", 4: "SIGILL", 5: "SIGTRAP", 6: "SIGABRT",
140
+ 7: "SIGBUS", 8: "SIGFPE", 9: "SIGKILL", 10: "SIGUSR1", 11: "SIGSEGV", 12: "SIGUSR2",
141
+ 13: "SIGPIPE", 14: "SIGALRM", 15: "SIGTERM", 17: "SIGCHLD", 18: "SIGCONT",
142
+ 19: "SIGSTOP", 20: "SIGTSTP",
143
+ };
144
+ return names[signal] ?? "SIGTERM";
145
+ }
28
146
  const STREAM_EMIT_DEBOUNCE_MS = 16;
29
147
  /** Min interval between full saveSession() calls for an in-progress streaming turn.
30
148
  * saveSession serializes the entire messages array, so doing it on every NDJSON
@@ -169,6 +287,7 @@ export class StructuredSessionManager {
169
287
  config;
170
288
  logger;
171
289
  sdkQueryFactory;
290
+ execHost;
172
291
  sessions = new Map();
173
292
  pendingRunnerExecutions = new Map();
174
293
  pendingSdkAbort = new Map();
@@ -210,18 +329,21 @@ export class StructuredSessionManager {
210
329
  grokRunner;
211
330
  qoderRunner;
212
331
  piRunner;
332
+ /** Structured CLI runs that were mid-flight when the previous web process died. */
333
+ pendingRecoveryIds = [];
213
334
  disposed = false;
214
- constructor(storage, config, logger = null, sdkQueryFactory = sdkQuery, runners = {}) {
335
+ constructor(storage, config, logger = null, sdkQueryFactory = sdkQuery, runners = {}, execHost) {
215
336
  this.storage = storage;
216
337
  this.config = config;
217
338
  this.logger = logger;
218
339
  this.sdkQueryFactory = sdkQueryFactory;
219
- this.claudeCliRunner = runners.claudeCli ?? new ClaudeCliRunner({ language: () => this.config.language });
220
- this.codexRunner = runners.codex ?? new CodexRunner();
221
- this.openCodeRunner = runners.opencode ?? new OpenCodeRunner();
222
- this.grokRunner = runners.grok ?? new GrokRunner();
223
- this.qoderRunner = runners.qoder ?? new QoderRunner();
224
- this.piRunner = runners.pi ?? new PiRunner();
340
+ this.execHost = execHost;
341
+ this.claudeCliRunner = runners.claudeCli ?? new ClaudeCliRunner({ language: () => this.config.language }, this.execHost);
342
+ this.codexRunner = runners.codex ?? new CodexRunner(undefined, this.execHost);
343
+ this.openCodeRunner = runners.opencode ?? new OpenCodeRunner(undefined, this.execHost);
344
+ this.grokRunner = runners.grok ?? new GrokRunner(undefined, this.execHost);
345
+ this.qoderRunner = runners.qoder ?? new QoderRunner(undefined, this.execHost);
346
+ this.piRunner = runners.pi ?? new PiRunner(undefined, this.execHost);
225
347
  for (const snapshot of this.storage.loadSessions()) {
226
348
  if ((snapshot.sessionKind ?? "pty") !== "structured")
227
349
  continue;
@@ -236,6 +358,9 @@ export class StructuredSessionManager {
236
358
  const runner = isStructuredRunnerForProvider(provider, storedRunner)
237
359
  ? storedRunner
238
360
  : defaultStructuredRunner(provider, this.config.structuredRunner);
361
+ if (snapshot.status === "running" && runner !== "claude-sdk") {
362
+ this.pendingRecoveryIds.push(snapshot.id);
363
+ }
239
364
  const restored = {
240
365
  ...snapshot,
241
366
  sessionKind: "structured",
@@ -316,10 +441,15 @@ export class StructuredSessionManager {
316
441
  .filter((session) => session.structuredState?.inFlight)
317
442
  .map((session) => session.id),
318
443
  ]);
444
+ // With a persistent exec host the daemon keeps CLI runs alive across web
445
+ // restarts; leave them in-flight so recoverDetachedRuns() can re-attach.
446
+ const detachSafe = this.execHost?.persistent === true;
319
447
  for (const id of activeSessionIds) {
320
448
  const session = this.sessions.get(id);
321
449
  if (!session)
322
450
  continue;
451
+ if (detachSafe && session.runner !== "claude-sdk")
452
+ continue;
323
453
  const cancelled = {
324
454
  ...session,
325
455
  status: "idle",
@@ -340,8 +470,11 @@ export class StructuredSessionManager {
340
470
  }
341
471
  catch { /* best-effort shutdown flush */ }
342
472
  }
343
- for (const execution of this.pendingRunnerExecutions.values())
473
+ for (const [executionId, execution] of this.pendingRunnerExecutions) {
474
+ if (detachSafe && this.sessions.get(executionId)?.runner !== "claude-sdk")
475
+ continue;
344
476
  execution.interrupt();
477
+ }
345
478
  for (const query of this.pendingSdkQueries.values()) {
346
479
  void query.interrupt().catch(() => { });
347
480
  }
@@ -361,6 +494,257 @@ export class StructuredSessionManager {
361
494
  this.topicCoordinator.clear();
362
495
  this.emitEvent = null;
363
496
  }
497
+ // ---------------------------------------------------------------------------
498
+ // Detached-run recovery: re-attach CLI runs that kept going inside terminald
499
+ // while the previous web process was down.
500
+ // ---------------------------------------------------------------------------
501
+ /** Called once after startup wiring; safe to skip when no host or candidates. */
502
+ async recoverDetachedRuns() {
503
+ if (this.disposed || this.execHost?.persistent !== true || this.pendingRecoveryIds.length === 0)
504
+ return;
505
+ const ids = this.pendingRecoveryIds;
506
+ this.pendingRecoveryIds = [];
507
+ let runs;
508
+ try {
509
+ runs = await this.execHost.listRuns();
510
+ }
511
+ catch (error) {
512
+ process.stderr.write(`[wand] structured run recovery skipped: ${getErrorMessage(error)}\n`);
513
+ return;
514
+ }
515
+ const byRunId = new Map(runs.map((run) => [run.runId, run]));
516
+ for (const sessionId of ids) {
517
+ const state = byRunId.get(structuredRunId(sessionId));
518
+ const session = this.sessions.get(sessionId);
519
+ if (!state || !session)
520
+ continue;
521
+ try {
522
+ await this.resumeDetachedRun(session, state);
523
+ }
524
+ catch (error) {
525
+ console.error(`[WAND] structured run recovery failed for ${sessionId}:`, error);
526
+ }
527
+ }
528
+ }
529
+ async resumeDetachedRun(snapshot, initialState) {
530
+ const sessionId = snapshot.id;
531
+ if (!this.execHost || !snapshot.structuredState)
532
+ return;
533
+ const requestId = `recover-${initialState.incarnationId}`;
534
+ // Re-arm the in-flight marker so UI and request guards treat the resumed
535
+ // turn like any other streaming turn.
536
+ const resumed = {
537
+ ...snapshot,
538
+ status: "running",
539
+ exitCode: null,
540
+ endedAt: null,
541
+ structuredState: {
542
+ ...snapshot.structuredState,
543
+ inFlight: true,
544
+ activeRequestId: requestId,
545
+ lastError: null,
546
+ },
547
+ };
548
+ this.sessions.set(sessionId, resumed);
549
+ this.saveAuthoritativeSession(resumed);
550
+ this.emitStructuredSnapshot(resumed);
551
+ process.stderr.write(`[wand] resuming structured run for session ${sessionId} (daemon pid ${initialState.pid})\n`);
552
+ const processor = buildReplayProcessor(resumed);
553
+ let emitTimer = null;
554
+ const syncTurn = (turnState) => {
555
+ const current = this.currentSessionForRequest(sessionId, requestId);
556
+ if (!current)
557
+ return;
558
+ const turn = {
559
+ role: "assistant",
560
+ content: this.compactContentBlocks([...turnState.blocks], turnState.result),
561
+ usage: turnState.usage,
562
+ };
563
+ const messages = [...(current.messages ?? [])];
564
+ if (messages[messages.length - 1]?.role === "assistant")
565
+ messages[messages.length - 1] = turn;
566
+ else
567
+ messages.push(turn);
568
+ const patched = {
569
+ ...current,
570
+ claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
571
+ messages,
572
+ output: turnState.result || current.output,
573
+ structuredState: {
574
+ ...current.structuredState,
575
+ model: turnState.model ?? current.structuredState?.model,
576
+ },
577
+ };
578
+ this.sessions.set(sessionId, patched);
579
+ this.saveStreamingSnapshot(patched);
580
+ };
581
+ const flushEmit = () => {
582
+ if (emitTimer)
583
+ this.clearStreamEmitTimer(emitTimer);
584
+ emitTimer = null;
585
+ const current = this.currentSessionForRequest(sessionId, requestId);
586
+ if (current) {
587
+ this.emit({ type: "output", sessionId, data: buildIncrementalStructuredPayload(current, this.config.cardDefaults ?? {}) });
588
+ }
589
+ };
590
+ const scheduleEmit = () => {
591
+ if (!emitTimer)
592
+ emitTimer = this.trackStreamEmitTimer(setTimeout(flushEmit, STREAM_EMIT_DEBOUNCE_MS));
593
+ };
594
+ const onApplied = (changed) => {
595
+ if (changed) {
596
+ syncTurn(processor.state);
597
+ scheduleEmit();
598
+ }
599
+ if (processor.askUserQuestionDetected && runningHandle) {
600
+ runningHandle.interrupt();
601
+ }
602
+ };
603
+ let runningHandle = null;
604
+ let fedChars = 0;
605
+ const snapshotChars = initialState.stdoutLog.length;
606
+ const feedLine = (line) => {
607
+ onApplied(processor.feed(line));
608
+ };
609
+ const feedDelta = (text) => {
610
+ // Skip the portion already covered by the attach snapshot to avoid
611
+ // double-feeding overlapping events buffered during adoption.
612
+ if (fedChars < snapshotChars) {
613
+ const remaining = snapshotChars - fedChars;
614
+ if (text.length <= remaining) {
615
+ fedChars += text.length;
616
+ return;
617
+ }
618
+ text = text.slice(remaining);
619
+ fedChars = snapshotChars;
620
+ }
621
+ fedChars += text.length;
622
+ carry += text;
623
+ const lines = carry.split("\n");
624
+ carry = lines.pop() ?? "";
625
+ for (const line of lines)
626
+ feedLine(line);
627
+ };
628
+ let carry = "";
629
+ const feedFullLog = () => {
630
+ const log = initialState.stdoutLog;
631
+ const lines = log.split("\n");
632
+ const tail = lines.pop() ?? "";
633
+ for (const line of lines)
634
+ feedLine(line);
635
+ if (tail.trim())
636
+ feedLine(tail);
637
+ };
638
+ if (initialState.status === "exited") {
639
+ feedFullLog();
640
+ this.finalizeRecoveredRun(sessionId, requestId, processor, {
641
+ exitCode: initialState.exitCode,
642
+ signal: initialState.signal === null ? null : numericToSignal(initialState.signal),
643
+ stderr: initialState.stderrLog,
644
+ stdoutTruncated: initialState.stdoutTruncated,
645
+ });
646
+ flushEmit();
647
+ return;
648
+ }
649
+ // Still running: replay what we have, then subscribe live. Events that
650
+ // arrive between snapshot and subscription are deduped via fedChars.
651
+ {
652
+ const lines = initialState.stdoutLog.split("\n");
653
+ carry = lines.pop() ?? "";
654
+ for (const line of lines)
655
+ feedLine(line);
656
+ fedChars = initialState.stdoutLog.length - carry.length;
657
+ }
658
+ runningHandle = await this.execHost.adoptRun(structuredRunId(sessionId));
659
+ if (!runningHandle) {
660
+ // Run vanished between listing and adoption; fall back to failure notes.
661
+ this.finalizeRecoveredRun(sessionId, requestId, processor, {
662
+ exitCode: null,
663
+ signal: null,
664
+ stderr: initialState.stderrLog,
665
+ stdoutTruncated: false,
666
+ lost: true,
667
+ });
668
+ flushEmit();
669
+ return;
670
+ }
671
+ runningHandle.onStream((event) => {
672
+ if (event.stream === "stdout")
673
+ feedDelta(event.data);
674
+ else
675
+ processor.stderr += event.data;
676
+ });
677
+ runningHandle.onExit((event) => {
678
+ if (carry.trim())
679
+ feedLine(carry);
680
+ carry = "";
681
+ this.finalizeRecoveredRun(sessionId, requestId, processor, {
682
+ exitCode: event.exitCode,
683
+ signal: event.signal === null ? null : numericToSignal(event.signal),
684
+ stderr: processor.stderr,
685
+ stdoutTruncated: initialState.stdoutTruncated,
686
+ });
687
+ flushEmit();
688
+ });
689
+ }
690
+ finalizeRecoveredRun(sessionId, requestId, processor, outcome) {
691
+ this.execHost?.forgetRun(structuredRunId(sessionId));
692
+ if (!this.isCurrentRequest(sessionId, requestId))
693
+ return;
694
+ const current = this.sessions.get(sessionId);
695
+ if (!current)
696
+ return;
697
+ const commandLabel = recoveredCommandLabel(current.runner);
698
+ const interruptedForQuestion = processor.stopReason === "ask-user-question";
699
+ const failedExit = outcome.lost
700
+ || outcome.stdoutTruncated
701
+ || (outcome.exitCode !== null && outcome.exitCode !== 0)
702
+ || outcome.signal !== null;
703
+ if ((processor.primaryError || failedExit) && !interruptedForQuestion) {
704
+ const errorText = outcome.lost
705
+ ? "服务重启后运行进程已丢失,本轮未能完成。"
706
+ : this.formatStructuredExitError(commandLabel, outcome.exitCode, outcome.signal, {
707
+ stderr: outcome.stderr.slice(-4096),
708
+ primary: processor.primaryError,
709
+ extras: processor.errors,
710
+ stdoutTail: processor.stdoutTail,
711
+ });
712
+ const failed = this.finishStructuredFailure(current, typeof outcome.exitCode === "number" ? outcome.exitCode : 1, errorText, processor.state);
713
+ this.sessions.set(sessionId, failed);
714
+ this.saveAuthoritativeSession(failed);
715
+ this.emitStructuredSnapshot(failed);
716
+ this.emitStructuredSnapshot(failed, "ended");
717
+ return;
718
+ }
719
+ const messages = this.buildCompletedAssistantMessages(current, processor.state);
720
+ const keepRunning = interruptedForQuestion;
721
+ const finished = {
722
+ ...current,
723
+ status: keepRunning ? "running" : "idle",
724
+ exitCode: keepRunning ? null : 0,
725
+ endedAt: keepRunning ? null : new Date().toISOString(),
726
+ output: processor.state.result,
727
+ claudeSessionId: processor.state.sessionId ?? current.claudeSessionId,
728
+ messages,
729
+ pendingEscalation: null,
730
+ permissionBlocked: false,
731
+ structuredState: {
732
+ ...current.structuredState,
733
+ model: processor.state.model ?? current.structuredState?.model,
734
+ inFlight: false,
735
+ activeRequestId: null,
736
+ lastError: null,
737
+ },
738
+ };
739
+ this.sessions.set(sessionId, finished);
740
+ this.saveAuthoritativeSession(finished);
741
+ this.emitStructuredSnapshot(finished);
742
+ if (!keepRunning)
743
+ this.emitStructuredSnapshot(finished, "ended");
744
+ if ((finished.queuedMessages?.length ?? 0) > 0) {
745
+ setImmediate(() => { void this.flushNextQueuedMessage(sessionId); });
746
+ }
747
+ }
364
748
  trackStreamEmitTimer(timer) {
365
749
  this.streamEmitTimers.add(timer);
366
750
  return timer;
@@ -1155,6 +1539,9 @@ export class StructuredSessionManager {
1155
1539
  if (this.pendingRunnerExecutions.get(sessionId) !== execution)
1156
1540
  return false;
1157
1541
  this.pendingRunnerExecutions.delete(sessionId);
1542
+ // Drop the daemon-side record once the manager has taken over completion;
1543
+ // no-op for the in-process fallback host.
1544
+ this.execHost?.forgetRun(structuredRunId(sessionId));
1158
1545
  return true;
1159
1546
  }
1160
1547
  releasePendingSdkAbort(sessionId, controller) {
@@ -1,6 +1,7 @@
1
1
  import { type TerminalDaemonRequest } from "./terminal-daemon-protocol.js";
2
2
  import { type TerminalAttachResult, type TerminalHost, type TerminalSpawnRequest } from "./terminal-host.js";
3
- export declare class TerminalDaemonClient implements TerminalHost {
3
+ import { type StructuredExecHost, type StructuredExecProcess, type StructuredSpawnRequest, type StructuredRunState } from "./structured-exec-host.js";
4
+ export declare class TerminalDaemonClient implements TerminalHost, StructuredExecHost {
4
5
  private readonly socketPath;
5
6
  private readonly token;
6
7
  readonly persistent = true;
@@ -11,6 +12,9 @@ export declare class TerminalDaemonClient implements TerminalHost {
11
12
  private readonly inventory;
12
13
  private readonly handles;
13
14
  private readonly pendingEvents;
15
+ private readonly structuredInventory;
16
+ private readonly structuredHandles;
17
+ private readonly pendingStructuredEvents;
14
18
  private disposed;
15
19
  private reconnectTimer;
16
20
  private reconnectDelayMs;
@@ -20,6 +24,14 @@ export declare class TerminalDaemonClient implements TerminalHost {
20
24
  attach(sessionId: string, afterSeq?: number): TerminalAttachResult | null;
21
25
  createOrAttach(request: TerminalSpawnRequest, afterSeq?: number): Promise<TerminalAttachResult>;
22
26
  forget(sessionId: string): void;
27
+ spawnStructured(request: StructuredSpawnRequest): Promise<StructuredExecProcess>;
28
+ attachRun(runId: string): Promise<StructuredRunState | null>;
29
+ adoptRun(runId: string): Promise<StructuredExecProcess | null>;
30
+ listRuns(): Promise<StructuredRunState[]>;
31
+ forgetRun(runId: string): void;
32
+ /** Refresh the structured inventory and catch live handles up after a reconnect. */
33
+ private refreshStructuredAfterReconnect;
34
+ private handleFromState;
23
35
  disconnect(): void;
24
36
  /**
25
37
  * Socket teardown path. Without a reconnect, a daemon restart would leave
@@ -42,6 +54,8 @@ export declare class TerminalDaemonClient implements TerminalHost {
42
54
  private resultFromState;
43
55
  private consume;
44
56
  private routeEvent;
57
+ /** Structured run events update the inventory and feed live handles. */
58
+ private routeStructuredEvent;
45
59
  private rejectPending;
46
60
  }
47
61
  export declare function createTerminalHost(configPath: string): Promise<TerminalHost>;