@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.
- package/dist/build-info.json +3 -3
- package/dist/distribution-manager.d.ts +17 -0
- package/dist/distribution-manager.js +80 -17
- package/dist/server-update-routes.d.ts +5 -0
- package/dist/server-update-routes.js +15 -0
- package/dist/server.js +8 -2
- package/dist/structured-claude-adapter.d.ts +3 -1
- package/dist/structured-claude-adapter.js +48 -93
- package/dist/structured-codex-adapter.d.ts +5 -0
- package/dist/structured-codex-adapter.js +40 -78
- package/dist/structured-exec-host.d.ts +76 -0
- package/dist/structured-exec-host.js +117 -0
- package/dist/structured-exec-pump.d.ts +37 -0
- package/dist/structured-exec-pump.js +127 -0
- package/dist/structured-grok-adapter.d.ts +3 -1
- package/dist/structured-grok-adapter.js +28 -67
- package/dist/structured-opencode-adapter.d.ts +3 -1
- package/dist/structured-opencode-adapter.js +37 -79
- package/dist/structured-pi-adapter.d.ts +3 -1
- package/dist/structured-pi-adapter.js +27 -39
- package/dist/structured-qoder-adapter.d.ts +3 -1
- package/dist/structured-qoder-adapter.js +49 -87
- package/dist/structured-session-manager.d.ts +9 -1
- package/dist/structured-session-manager.js +399 -12
- package/dist/terminal-daemon-client.d.ts +15 -1
- package/dist/terminal-daemon-client.js +261 -0
- package/dist/terminal-daemon-protocol.d.ts +10 -3
- package/dist/terminal-daemon-protocol.js +1 -1
- package/dist/terminal-daemon-server.js +178 -0
- package/dist/web-ui/content/scripts.js +57 -57
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- 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.
|
|
220
|
-
this.
|
|
221
|
-
this.
|
|
222
|
-
this.
|
|
223
|
-
this.
|
|
224
|
-
this.
|
|
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
|
|
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
|
-
|
|
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>;
|