@co0ontty/wand 4.72.2 → 4.75.0-beta.g1d477a2
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 +4 -4
- package/dist/claude-sdk-runner.d.ts +2 -3
- package/dist/config.d.ts +1 -1
- package/dist/config.js +48 -0
- package/dist/daemon-connection.d.ts +9 -0
- package/dist/daemon-connection.js +86 -0
- package/dist/distribution-manager.d.ts +5 -0
- package/dist/distribution-manager.js +79 -1
- package/dist/git-quick-commit.js +12 -0
- package/dist/message-truncator.js +9 -3
- package/dist/models.d.ts +39 -2
- package/dist/models.js +302 -9
- package/dist/native/darwin-arm64/wand-render +0 -0
- package/dist/native/darwin-arm64/wand-render.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-render.version +1 -0
- package/dist/native/darwin-arm64/wand-structured-renderd +0 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/darwin-x64/wand-render +0 -0
- package/dist/native/darwin-x64/wand-render.sha256 +1 -0
- package/dist/native/darwin-x64/wand-render.version +1 -0
- package/dist/native/darwin-x64/wand-structured-renderd +0 -0
- package/dist/native/darwin-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/darwin-x64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-arm64/wand-render +0 -0
- package/dist/native/linux-arm64/wand-render.sha256 +1 -0
- package/dist/native/linux-arm64/wand-render.version +1 -0
- package/dist/native/linux-arm64/wand-structured-renderd +0 -0
- package/dist/native/linux-arm64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-arm64/wand-structured-renderd.version +1 -0
- package/dist/native/linux-x64/wand-render +0 -0
- package/dist/native/linux-x64/wand-render.sha256 +1 -0
- package/dist/native/linux-x64/wand-render.version +1 -0
- package/dist/native/linux-x64/wand-structured-renderd +0 -0
- package/dist/native/linux-x64/wand-structured-renderd.sha256 +1 -0
- package/dist/native/linux-x64/wand-structured-renderd.version +1 -0
- package/dist/process-manager.d.ts +12 -1
- package/dist/process-manager.js +88 -7
- package/dist/pty-terminal-state.d.ts +17 -0
- package/dist/pty-terminal-state.js +51 -1
- package/dist/render-binary.d.ts +61 -0
- package/dist/render-binary.js +204 -0
- package/dist/render-daemon-client.d.ts +202 -0
- package/dist/render-daemon-client.js +1164 -0
- package/dist/render-host.d.ts +88 -0
- package/dist/render-host.js +440 -0
- package/dist/render-protocol.d.ts +128 -0
- package/dist/render-protocol.js +99 -0
- package/dist/render-structured-client.d.ts +80 -0
- package/dist/render-structured-client.js +513 -0
- package/dist/render-structured-host.d.ts +31 -0
- package/dist/render-structured-host.js +218 -0
- package/dist/render-structured-protocol.d.ts +101 -0
- package/dist/render-structured-protocol.js +35 -0
- package/dist/server-session-routes.js +114 -12
- package/dist/server-task-routes.js +55 -6
- package/dist/server-update-routes.d.ts +2 -0
- package/dist/server-update-routes.js +44 -0
- package/dist/server.js +43 -13
- package/dist/session-ai-context.d.ts +1 -1
- package/dist/session-ai-context.js +15 -4
- package/dist/session-logger.d.ts +2 -0
- package/dist/session-logger.js +44 -1
- package/dist/session-transport.js +1 -1
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +14 -5
- package/dist/structured-client-protocol.d.ts +3 -1
- package/dist/structured-client-protocol.js +85 -25
- package/dist/structured-content.d.ts +20 -3
- package/dist/structured-content.js +80 -1
- package/dist/structured-exec-host.js +7 -1
- package/dist/structured-exec-pump.js +6 -2
- package/dist/structured-grok-adapter.js +22 -0
- package/dist/structured-pi-adapter.js +18 -2
- package/dist/structured-provider-common.d.ts +11 -9
- package/dist/structured-provider-common.js +60 -18
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +27 -3
- package/dist/task-types.d.ts +4 -1
- package/dist/terminal-daemon-client.d.ts +9 -2
- package/dist/terminal-daemon-client.js +119 -46
- package/dist/terminal-daemon-server.js +47 -8
- package/dist/terminal-host.d.ts +6 -0
- package/dist/terminal-host.js +3 -0
- package/dist/tui/commands.js +183 -10
- package/dist/tui/ipc-server.js +12 -0
- package/dist/types.d.ts +30 -10
- package/dist/unix-socket-keepalive.d.ts +3 -0
- package/dist/unix-socket-keepalive.js +94 -0
- package/dist/wand-task-sync.js +2 -1
- package/dist/web-ui/content/scripts.js +79 -73
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +21 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/thinking-efforts.d.ts +24 -0
- package/dist/web-ui/thinking-efforts.js +81 -0
- package/dist/ws-broadcast.d.ts +1 -1
- package/dist/ws-broadcast.js +28 -9
- package/package.json +8 -3
- package/scripts/install-render-binary.js +499 -0
- package/scripts/stage-render-binaries.js +626 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { RenderStructuredClient } from "./render-structured-client.js";
|
|
2
|
+
import type { StructuredExecHost, StructuredExecProcess, StructuredRunState, StructuredSpawnRequest } from "./structured-exec-host.js";
|
|
3
|
+
type Owner = "legacy" | "rust";
|
|
4
|
+
/** Resolve the separate v2 executable; do not confuse it with PTY v1 wand-render. */
|
|
5
|
+
export declare function resolveStructuredRenderBinary(configPath: string): string | null;
|
|
6
|
+
/**
|
|
7
|
+
* A stable runId can belong to exactly one persistent daemon. Configuration
|
|
8
|
+
* chooses only new runs; existing owners are always resolved by inventories.
|
|
9
|
+
*/
|
|
10
|
+
export declare class CompositeStructuredExecHost implements StructuredExecHost {
|
|
11
|
+
private readonly legacy;
|
|
12
|
+
private readonly rust;
|
|
13
|
+
private readonly newOwner;
|
|
14
|
+
readonly persistent = true;
|
|
15
|
+
private readonly owners;
|
|
16
|
+
private readonly starting;
|
|
17
|
+
constructor(legacy: StructuredExecHost | null, rust: StructuredExecHost | null, newOwner: Owner);
|
|
18
|
+
private getHost;
|
|
19
|
+
listRuns(): Promise<StructuredRunState[]>;
|
|
20
|
+
private ownerOf;
|
|
21
|
+
spawnStructured(request: StructuredSpawnRequest): Promise<StructuredExecProcess>;
|
|
22
|
+
attachRun(runId: string): Promise<StructuredRunState | null>;
|
|
23
|
+
adoptRun(runId: string): Promise<StructuredExecProcess | null>;
|
|
24
|
+
forgetRun(runId: string): void;
|
|
25
|
+
}
|
|
26
|
+
/** Default legacy. v2 is adopted even on rollback when it still owns runs. */
|
|
27
|
+
export declare function createUpgradeAwareStructuredHost(configPath: string, legacy: StructuredExecHost | null, processHost?: "legacy" | "rust"): Promise<{
|
|
28
|
+
host: StructuredExecHost | undefined;
|
|
29
|
+
rustClient: RenderStructuredClient | null;
|
|
30
|
+
}>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, unlinkSync } from "node:fs";
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
6
|
+
import { getErrorMessage } from "./error-utils.js";
|
|
7
|
+
import { currentRenderTriple, isExecutableFile } from "./render-binary.js";
|
|
8
|
+
import { RenderStructuredClient } from "./render-structured-client.js";
|
|
9
|
+
import { structuredRenderPaths } from "./render-structured-protocol.js";
|
|
10
|
+
const READY_TIMEOUT_MS = 5_000;
|
|
11
|
+
/** Resolve the separate v2 executable; do not confuse it with PTY v1 wand-render. */
|
|
12
|
+
export function resolveStructuredRenderBinary(configPath) {
|
|
13
|
+
const explicit = process.env.WAND_STRUCTURED_RENDER_BIN?.trim();
|
|
14
|
+
if (explicit) {
|
|
15
|
+
if (!isExecutableFile(explicit))
|
|
16
|
+
throw new Error("WAND_STRUCTURED_RENDER_BIN is not executable");
|
|
17
|
+
return explicit;
|
|
18
|
+
}
|
|
19
|
+
const binary = process.platform === "win32" ? "wand-structured-renderd.exe" : "wand-structured-renderd";
|
|
20
|
+
const root = path.resolve(import.meta.dirname, "..");
|
|
21
|
+
const choices = [
|
|
22
|
+
path.join(root, "render", "target", "release", binary),
|
|
23
|
+
path.join(root, "render", "target", "debug", binary),
|
|
24
|
+
path.join(path.dirname(configPath), "bin", binary),
|
|
25
|
+
path.join(root, "dist", "native", currentRenderTriple(), binary),
|
|
26
|
+
...(process.env.PATH ?? "").split(path.delimiter).filter(Boolean).map((dir) => path.join(dir, binary)),
|
|
27
|
+
];
|
|
28
|
+
return choices.find(isExecutableFile) ?? null;
|
|
29
|
+
}
|
|
30
|
+
function livePid(file) {
|
|
31
|
+
try {
|
|
32
|
+
const pid = Number(readFileSync(file, "utf8").trim());
|
|
33
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
34
|
+
return null;
|
|
35
|
+
process.kill(pid, 0);
|
|
36
|
+
return pid;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return error.code === "EPERM" ? -1 : null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function socketIsListening(socketPath) {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
const socket = net.createConnection(socketPath);
|
|
45
|
+
let done = false;
|
|
46
|
+
const finish = (listening) => {
|
|
47
|
+
if (done)
|
|
48
|
+
return;
|
|
49
|
+
done = true;
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
socket.destroy();
|
|
52
|
+
resolve(listening);
|
|
53
|
+
};
|
|
54
|
+
const timer = setTimeout(() => finish(false), 250);
|
|
55
|
+
socket.once("connect", () => finish(true));
|
|
56
|
+
socket.once("error", () => finish(false));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
async function connectExisting(configPath) {
|
|
60
|
+
const paths = structuredRenderPaths(configPath);
|
|
61
|
+
if (!existsSync(paths.socketPath))
|
|
62
|
+
return null;
|
|
63
|
+
const client = new RenderStructuredClient(configPath);
|
|
64
|
+
try {
|
|
65
|
+
await client.connect();
|
|
66
|
+
return client;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
client.disconnect();
|
|
70
|
+
if (await socketIsListening(paths.socketPath) || livePid(paths.pidPath) !== null) {
|
|
71
|
+
throw new Error(`Structured Render endpoint exists but refused adoption: ${getErrorMessage(error)}`);
|
|
72
|
+
}
|
|
73
|
+
// Only remove our own private, unlistened stale socket. Never unlink a
|
|
74
|
+
// foreign path or race a live daemon's startup/credential rotation.
|
|
75
|
+
const stat = lstatSync(paths.socketPath);
|
|
76
|
+
const ownUid = typeof process.getuid !== "function" || stat.uid === process.getuid();
|
|
77
|
+
if (!stat.isSocket() || !ownUid || (stat.mode & 0o777) !== 0o600) {
|
|
78
|
+
throw new Error("Structured Render endpoint is not a private socket; refusing cleanup");
|
|
79
|
+
}
|
|
80
|
+
unlinkSync(paths.socketPath);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function startV2(configPath) {
|
|
85
|
+
const existing = await connectExisting(configPath);
|
|
86
|
+
if (existing)
|
|
87
|
+
return existing;
|
|
88
|
+
const paths = structuredRenderPaths(configPath);
|
|
89
|
+
if (livePid(paths.pidPath) !== null) {
|
|
90
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
91
|
+
while (Date.now() < deadline) {
|
|
92
|
+
const starting = await connectExisting(configPath);
|
|
93
|
+
if (starting)
|
|
94
|
+
return starting;
|
|
95
|
+
await delay(50);
|
|
96
|
+
}
|
|
97
|
+
throw new Error("Structured Render PID is alive but its socket is unavailable; refusing a competing daemon");
|
|
98
|
+
}
|
|
99
|
+
const binary = resolveStructuredRenderBinary(configPath);
|
|
100
|
+
if (!binary)
|
|
101
|
+
throw new Error("structured.processHost=rust requires a v2 binary; build wand-structured-renderd or set WAND_STRUCTURED_RENDER_BIN");
|
|
102
|
+
const child = spawn(binary, ["--config", configPath], {
|
|
103
|
+
detached: true, stdio: "ignore", cwd: process.cwd(), env: process.env,
|
|
104
|
+
});
|
|
105
|
+
child.on("error", (error) => process.stderr.write(`[wand] Structured Render launch failed: ${getErrorMessage(error)}\n`));
|
|
106
|
+
child.unref();
|
|
107
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
108
|
+
while (Date.now() < deadline) {
|
|
109
|
+
try {
|
|
110
|
+
const client = await connectExisting(configPath);
|
|
111
|
+
if (client)
|
|
112
|
+
return client;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
// A live but incompatible owner must not be replaced or silently downgraded.
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
await delay(50);
|
|
119
|
+
}
|
|
120
|
+
if (child.pid) {
|
|
121
|
+
try {
|
|
122
|
+
process.kill(child.pid, "SIGTERM");
|
|
123
|
+
}
|
|
124
|
+
catch { /* child already exited */ }
|
|
125
|
+
}
|
|
126
|
+
throw new Error("Structured Render v2 did not become ready; refusing legacy fallback");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* A stable runId can belong to exactly one persistent daemon. Configuration
|
|
130
|
+
* chooses only new runs; existing owners are always resolved by inventories.
|
|
131
|
+
*/
|
|
132
|
+
export class CompositeStructuredExecHost {
|
|
133
|
+
legacy;
|
|
134
|
+
rust;
|
|
135
|
+
newOwner;
|
|
136
|
+
persistent = true;
|
|
137
|
+
owners = new Map();
|
|
138
|
+
starting = new Map();
|
|
139
|
+
constructor(legacy, rust, newOwner) {
|
|
140
|
+
this.legacy = legacy;
|
|
141
|
+
this.rust = rust;
|
|
142
|
+
this.newOwner = newOwner;
|
|
143
|
+
if (!legacy && !rust)
|
|
144
|
+
throw new Error("at least one structured daemon is required");
|
|
145
|
+
}
|
|
146
|
+
getHost(owner) {
|
|
147
|
+
return owner === "legacy" ? this.legacy : this.rust;
|
|
148
|
+
}
|
|
149
|
+
async listRuns() {
|
|
150
|
+
const [legacyRuns, rustRuns] = await Promise.all([
|
|
151
|
+
this.legacy?.listRuns() ?? Promise.resolve([]),
|
|
152
|
+
this.rust?.listRuns() ?? Promise.resolve([]),
|
|
153
|
+
]);
|
|
154
|
+
const legacyIds = new Set(legacyRuns.map((run) => run.runId));
|
|
155
|
+
for (const run of rustRuns) {
|
|
156
|
+
if (legacyIds.has(run.runId))
|
|
157
|
+
throw new Error(`structured run ${run.runId} has competing owners; refusing adoption`);
|
|
158
|
+
}
|
|
159
|
+
for (const run of legacyRuns)
|
|
160
|
+
this.owners.set(run.runId, "legacy");
|
|
161
|
+
for (const run of rustRuns)
|
|
162
|
+
this.owners.set(run.runId, "rust");
|
|
163
|
+
return [...legacyRuns, ...rustRuns];
|
|
164
|
+
}
|
|
165
|
+
async ownerOf(runId) {
|
|
166
|
+
const cached = this.owners.get(runId);
|
|
167
|
+
if (cached)
|
|
168
|
+
return cached;
|
|
169
|
+
await this.listRuns();
|
|
170
|
+
return this.owners.get(runId) ?? null;
|
|
171
|
+
}
|
|
172
|
+
async spawnStructured(request) {
|
|
173
|
+
const pending = this.starting.get(request.runId);
|
|
174
|
+
if (pending)
|
|
175
|
+
return pending;
|
|
176
|
+
const attempt = (async () => {
|
|
177
|
+
const owner = (await this.ownerOf(request.runId)) ?? this.newOwner;
|
|
178
|
+
const host = this.getHost(owner);
|
|
179
|
+
if (!host)
|
|
180
|
+
throw new Error(`structured owner ${owner} is unavailable; no fallback across owners`);
|
|
181
|
+
const handle = await host.spawnStructured(request);
|
|
182
|
+
this.owners.set(request.runId, owner);
|
|
183
|
+
return handle;
|
|
184
|
+
})();
|
|
185
|
+
this.starting.set(request.runId, attempt);
|
|
186
|
+
try {
|
|
187
|
+
return await attempt;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
if (this.starting.get(request.runId) === attempt)
|
|
191
|
+
this.starting.delete(request.runId);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async attachRun(runId) {
|
|
195
|
+
const owner = await this.ownerOf(runId);
|
|
196
|
+
return owner ? this.getHost(owner)?.attachRun(runId) ?? null : null;
|
|
197
|
+
}
|
|
198
|
+
async adoptRun(runId) {
|
|
199
|
+
const owner = await this.ownerOf(runId);
|
|
200
|
+
return owner ? this.getHost(owner)?.adoptRun(runId) ?? null : null;
|
|
201
|
+
}
|
|
202
|
+
forgetRun(runId) {
|
|
203
|
+
const owner = this.owners.get(runId);
|
|
204
|
+
this.owners.delete(runId);
|
|
205
|
+
if (owner)
|
|
206
|
+
this.getHost(owner)?.forgetRun(runId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Default legacy. v2 is adopted even on rollback when it still owns runs. */
|
|
210
|
+
export async function createUpgradeAwareStructuredHost(configPath, legacy, processHost = "legacy") {
|
|
211
|
+
if (process.env.WAND_TEST_MODE === "1" || process.env.NODE_TEST_CONTEXT) {
|
|
212
|
+
return { host: legacy ?? undefined, rustClient: null };
|
|
213
|
+
}
|
|
214
|
+
const rustClient = processHost === "rust" ? await startV2(configPath) : await connectExisting(configPath);
|
|
215
|
+
if (!rustClient && !legacy)
|
|
216
|
+
return { host: undefined, rustClient: null };
|
|
217
|
+
return { host: new CompositeStructuredExecHost(legacy, rustClient, processHost), rustClient };
|
|
218
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** Separate structured process daemon. v1 PTY socket and wire format are unchanged. */
|
|
2
|
+
export declare const STRUCTURED_RENDER_PROTOCOL_VERSION = 2;
|
|
3
|
+
export declare const STRUCTURED_RENDER_RUN_LOG_MAX_BYTES: number;
|
|
4
|
+
export declare const STRUCTURED_RENDER_REPLAY_PAGE_MAX_BYTES: number;
|
|
5
|
+
/** Envelope and length-prefixed JSON framing are identical to Render v1. */
|
|
6
|
+
export { encodeRenderFrame, decodeRenderFrames } from "./render-protocol.js";
|
|
7
|
+
export type { RenderResponse, RenderErrorCode } from "./render-protocol.js";
|
|
8
|
+
export interface StructuredRenderRunState {
|
|
9
|
+
runId: string;
|
|
10
|
+
incarnationId: string;
|
|
11
|
+
pid: number;
|
|
12
|
+
status: "running" | "exited";
|
|
13
|
+
exitCode: number | null;
|
|
14
|
+
signal: number | null;
|
|
15
|
+
stdoutSeq: number;
|
|
16
|
+
stderrSeq: number;
|
|
17
|
+
stdoutTruncated: boolean;
|
|
18
|
+
stderrTruncated: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface StructuredRenderSpawnParams {
|
|
21
|
+
runId: string;
|
|
22
|
+
file: string;
|
|
23
|
+
args: string[];
|
|
24
|
+
cwd: string;
|
|
25
|
+
env: Record<string, string>;
|
|
26
|
+
stdinData?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface StructuredRenderSpawnResult {
|
|
29
|
+
state: StructuredRenderRunState;
|
|
30
|
+
isNew: boolean;
|
|
31
|
+
}
|
|
32
|
+
/** Inventory deliberately carries no raw bytes, prompt, argv, cwd or env. */
|
|
33
|
+
export interface StructuredRenderListResult {
|
|
34
|
+
runs: StructuredRenderRunState[];
|
|
35
|
+
}
|
|
36
|
+
export interface StructuredRenderAttachParams {
|
|
37
|
+
runId: string;
|
|
38
|
+
afterStdoutSeq?: number;
|
|
39
|
+
afterStderrSeq?: number;
|
|
40
|
+
maxBytes?: number;
|
|
41
|
+
}
|
|
42
|
+
export interface StructuredRenderReplayStream {
|
|
43
|
+
chunks: {
|
|
44
|
+
seq: number;
|
|
45
|
+
data: string;
|
|
46
|
+
}[];
|
|
47
|
+
nextSeq: number;
|
|
48
|
+
complete: boolean;
|
|
49
|
+
resetRequired: boolean;
|
|
50
|
+
}
|
|
51
|
+
export interface StructuredRenderAttachResult {
|
|
52
|
+
state: StructuredRenderRunState;
|
|
53
|
+
stdout: StructuredRenderReplayStream;
|
|
54
|
+
stderr: StructuredRenderReplayStream;
|
|
55
|
+
}
|
|
56
|
+
export interface StructuredRenderHelloResult {
|
|
57
|
+
version: string;
|
|
58
|
+
protocolVersion: number;
|
|
59
|
+
pid: number;
|
|
60
|
+
startedAt: string;
|
|
61
|
+
runs: number;
|
|
62
|
+
}
|
|
63
|
+
export interface StructuredRenderStatsResult {
|
|
64
|
+
runs: number;
|
|
65
|
+
runningRuns: number;
|
|
66
|
+
retainedLogBytes: number;
|
|
67
|
+
rssBytes: number;
|
|
68
|
+
}
|
|
69
|
+
export type StructuredRenderEvent = {
|
|
70
|
+
event: "stream";
|
|
71
|
+
runId: string;
|
|
72
|
+
incarnationId: string;
|
|
73
|
+
stream: "stdout" | "stderr";
|
|
74
|
+
seq: number;
|
|
75
|
+
data: string;
|
|
76
|
+
} | {
|
|
77
|
+
event: "exit";
|
|
78
|
+
runId: string;
|
|
79
|
+
incarnationId: string;
|
|
80
|
+
exitCode: number | null;
|
|
81
|
+
signal: number | null;
|
|
82
|
+
} | {
|
|
83
|
+
event: "reconcile";
|
|
84
|
+
runIds: string[];
|
|
85
|
+
};
|
|
86
|
+
export type StructuredRenderMethod = "hello" | "ping" | "list" | "spawn" | "attach" | "interrupt" | "forget" | "stats" | "shutdown";
|
|
87
|
+
export interface StructuredRenderRequest {
|
|
88
|
+
id: number;
|
|
89
|
+
token: string;
|
|
90
|
+
protocolVersion: number;
|
|
91
|
+
method: StructuredRenderMethod;
|
|
92
|
+
params?: Record<string, unknown>;
|
|
93
|
+
}
|
|
94
|
+
export interface StructuredRenderPaths {
|
|
95
|
+
socketPath: string;
|
|
96
|
+
tokenPath: string;
|
|
97
|
+
pidPath: string;
|
|
98
|
+
metaPath: string;
|
|
99
|
+
}
|
|
100
|
+
/** Mirrors Rust normalized_absolute() and config_suffix(), in an independent namespace. */
|
|
101
|
+
export declare function structuredRenderPaths(configPath: string): StructuredRenderPaths;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
/** Separate structured process daemon. v1 PTY socket and wire format are unchanged. */
|
|
6
|
+
export const STRUCTURED_RENDER_PROTOCOL_VERSION = 2;
|
|
7
|
+
export const STRUCTURED_RENDER_RUN_LOG_MAX_BYTES = 8 * 1024 * 1024;
|
|
8
|
+
export const STRUCTURED_RENDER_REPLAY_PAGE_MAX_BYTES = 512 * 1024;
|
|
9
|
+
/** Envelope and length-prefixed JSON framing are identical to Render v1. */
|
|
10
|
+
export { encodeRenderFrame, decodeRenderFrames } from "./render-protocol.js";
|
|
11
|
+
/** Mirrors Rust normalized_absolute() and config_suffix(), in an independent namespace. */
|
|
12
|
+
export function structuredRenderPaths(configPath) {
|
|
13
|
+
const absolute = path.resolve(configPath);
|
|
14
|
+
let resolved = absolute;
|
|
15
|
+
try {
|
|
16
|
+
resolved = realpathSync(absolute);
|
|
17
|
+
}
|
|
18
|
+
catch { /* config may not exist yet */ }
|
|
19
|
+
const suffix = createHash("sha256").update(resolved).digest("hex").slice(0, 12);
|
|
20
|
+
const dir = path.dirname(resolved);
|
|
21
|
+
const uid = (() => {
|
|
22
|
+
try {
|
|
23
|
+
return os.userInfo().uid;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
})();
|
|
29
|
+
return {
|
|
30
|
+
socketPath: path.join("/tmp", `wand-structured-render-${uid}-${suffix}.sock`),
|
|
31
|
+
tokenPath: path.join(dir, `.structured-render-${suffix}.token`),
|
|
32
|
+
pidPath: path.join(dir, `.structured-render-${suffix}.pid`),
|
|
33
|
+
metaPath: path.join(dir, `.structured-render-${suffix}.json`),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { SessionInputError } from "./process-manager.js";
|
|
2
|
+
import { PtyInputDeliveryError, SessionInputError } from "./process-manager.js";
|
|
3
3
|
import { getDefaultModelForProvider, isExecutionMode } from "./config.js";
|
|
4
4
|
import { blockWindowMessagesForTransport, sliceTurnBlocksForTransport, truncateMessagesForTransport, windowMessagesForTransport } from "./message-truncator.js";
|
|
5
5
|
import { toSessionDetailDTO, toSessionListItemDTO } from "./session-transport.js";
|
|
@@ -17,6 +17,7 @@ import { sendRouteError } from "./server-request.js";
|
|
|
17
17
|
import { registerStructuredResumeRoutes } from "./server-resume-routes.js";
|
|
18
18
|
import { addHiddenSessionIds, removeHiddenSessionIds } from "./session-registry.js";
|
|
19
19
|
import { enrichStructuredMessages, WAND_PROTOCOL_VERSION } from "./structured-client-protocol.js";
|
|
20
|
+
import { asRecord, isStructuredImagePart } from "./structured-content.js";
|
|
20
21
|
import { syncWorkspaceTaskToBoard } from "./wand-task-sync.js";
|
|
21
22
|
import { buildDirectoryTree, normalizeSessionDirectory, } from "./session-directory-tree.js";
|
|
22
23
|
import { projectCwdForSession, renameWorkspaceDirectory, resolveWorkspaceIdForNewSession, } from "./workspace-binding.js";
|
|
@@ -64,6 +65,17 @@ export function parseSessionCreationOrigin(body) {
|
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
function getInputErrorResponse(error, sessionId) {
|
|
68
|
+
if (error instanceof PtyInputDeliveryError) {
|
|
69
|
+
return {
|
|
70
|
+
statusCode: 503,
|
|
71
|
+
payload: {
|
|
72
|
+
error: "PTY 输入结果未获确认;请先检查终端输出,不要直接重复提交。",
|
|
73
|
+
errorCode: "INPUT_DELIVERY_UNCONFIRMED",
|
|
74
|
+
sessionId,
|
|
75
|
+
sessionStatus: null,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
67
79
|
if (error instanceof SessionInputError) {
|
|
68
80
|
const statusCode = error.code === "SESSION_NOT_FOUND" ? 404 : 409;
|
|
69
81
|
return {
|
|
@@ -351,7 +363,7 @@ function isMergeActionAllowed(snapshot) {
|
|
|
351
363
|
}
|
|
352
364
|
export function registerSessionRoutes(app, processes, structured, storage, defaultMode, config, sessions, onSessionCreated) {
|
|
353
365
|
const sessionResponseDTO = (snapshot) => {
|
|
354
|
-
const windowed = windowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? []), config.cardDefaults ?? {});
|
|
366
|
+
const windowed = windowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? [], snapshot.id), config.cardDefaults ?? {});
|
|
355
367
|
return toSessionDetailDTO(snapshot, {
|
|
356
368
|
messages: windowed.messages,
|
|
357
369
|
messageOffset: windowed.messageOffset,
|
|
@@ -565,7 +577,15 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
565
577
|
res.status(404).json({ error: "未找到该结构化会话。" });
|
|
566
578
|
return;
|
|
567
579
|
}
|
|
568
|
-
|
|
580
|
+
const all = snapshot.messages ?? [];
|
|
581
|
+
const offset = parseBoundedInteger(req.query.offset, Math.max(0, all.length - 40), 0, all.length);
|
|
582
|
+
const limit = parseBoundedInteger(req.query.limit, 40, 1, 200);
|
|
583
|
+
res.json({
|
|
584
|
+
id: snapshot.id,
|
|
585
|
+
messages: truncateMessagesForTransport(all.slice(offset, offset + limit), config.cardDefaults ?? {}),
|
|
586
|
+
offset,
|
|
587
|
+
total: all.length,
|
|
588
|
+
});
|
|
569
589
|
});
|
|
570
590
|
app.post("/api/structured-sessions/:id/messages", asyncRoute(async (req, res) => {
|
|
571
591
|
const input = String(req.body?.input ?? "");
|
|
@@ -620,6 +640,19 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
620
640
|
sendRouteError(res, error, "无法调整排队顺序。");
|
|
621
641
|
}
|
|
622
642
|
});
|
|
643
|
+
app.patch("/api/structured-sessions/:id/queued/:index", (req, res) => {
|
|
644
|
+
const { expectedText, text } = req.body ?? {};
|
|
645
|
+
if (typeof expectedText !== "string" || typeof text !== "string" || !text.trim()) {
|
|
646
|
+
res.status(400).json({ error: "排队消息不能为空,且必须提供原文。" });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
try {
|
|
650
|
+
res.json(sessionResponseDTO(structured.editQueuedMessage(req.params.id, Number(req.params.index), expectedText, text)));
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
sendRouteError(res, error, "无法编辑排队消息。");
|
|
654
|
+
}
|
|
655
|
+
});
|
|
623
656
|
app.delete("/api/structured-sessions/:id/queued/:index", (req, res) => {
|
|
624
657
|
const index = Number(req.params.index);
|
|
625
658
|
if (!Number.isFinite(index)) {
|
|
@@ -688,6 +721,41 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
688
721
|
}
|
|
689
722
|
res.status(404).json({ error: "未找到该工具结果。" });
|
|
690
723
|
});
|
|
724
|
+
// ── Inline tool image endpoint ──
|
|
725
|
+
// tool_result 里内联的 base64 图片在传输层被改写为这个 URL(避免把整张图的 base64
|
|
726
|
+
// 塞进每次快照 / WS 帧)。各端用同一套带鉴权的取图通道按需加载。
|
|
727
|
+
app.get("/api/sessions/:id/tool-images/:toolUseId/:index", (req, res) => {
|
|
728
|
+
const snapshot = sessions.get(req.params.id);
|
|
729
|
+
if (!snapshot) {
|
|
730
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const toolUseId = req.params.toolUseId;
|
|
734
|
+
const index = Number.parseInt(req.params.index, 10);
|
|
735
|
+
if (!Number.isFinite(index) || index < 0) {
|
|
736
|
+
res.status(400).json({ error: "无效的图片序号。" });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
for (const turn of snapshot.messages ?? []) {
|
|
740
|
+
for (const block of turn.content) {
|
|
741
|
+
if (block.type !== "tool_result" || block.tool_use_id !== toolUseId || !Array.isArray(block.content))
|
|
742
|
+
continue;
|
|
743
|
+
const part = block.content.filter(isStructuredImagePart)[index];
|
|
744
|
+
const source = asRecord(part?.source);
|
|
745
|
+
const data = typeof source?.data === "string" ? source.data : undefined;
|
|
746
|
+
if (source?.type !== "base64" || !data) {
|
|
747
|
+
res.status(404).json({ error: "未找到该图片。" });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
res.setHeader("Content-Type", typeof source.media_type === "string" ? source.media_type : "image/png");
|
|
751
|
+
res.setHeader("Cache-Control", "private, max-age=3600");
|
|
752
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
753
|
+
res.send(Buffer.from(data, "base64"));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
res.status(404).json({ error: "未找到该工具结果。" });
|
|
758
|
+
});
|
|
691
759
|
app.post("/api/sessions/:id/worktree/merge/check", asyncRoute(async (req, res) => {
|
|
692
760
|
try {
|
|
693
761
|
const current = requireWorktreeSession(sessions.getLatest(req.params.id));
|
|
@@ -1001,6 +1069,25 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1001
1069
|
}
|
|
1002
1070
|
res.json({ ok: true, deleted, failed });
|
|
1003
1071
|
});
|
|
1072
|
+
app.get("/api/sessions/:id/pty-history", asyncRoute(async (req, res) => {
|
|
1073
|
+
const snapshot = sessions.get(req.params.id);
|
|
1074
|
+
if (!snapshot || (snapshot.sessionKind ?? "pty") !== "pty") {
|
|
1075
|
+
res.status(404).json({ error: "未找到 PTY 会话。" });
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const before = Number(req.query.before);
|
|
1079
|
+
const revision = Number(req.query.revision);
|
|
1080
|
+
if (!Number.isSafeInteger(before) || !Number.isSafeInteger(revision) || before < 1 || revision < 0) {
|
|
1081
|
+
res.status(400).json({ error: "无效的终端历史游标。" });
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const page = await processes.getTerminalHistoryPage(snapshot.id, before, revision);
|
|
1085
|
+
if (!page) {
|
|
1086
|
+
res.status(409).json({ error: "终端历史已变化,请重新同步。" });
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
res.json(page);
|
|
1090
|
+
}));
|
|
1004
1091
|
app.get("/api/sessions/:id", (req, res) => {
|
|
1005
1092
|
const snapshot = sessions.get(req.params.id);
|
|
1006
1093
|
if (!snapshot) {
|
|
@@ -1011,12 +1098,12 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1011
1098
|
? processes.getPtyTranscript(snapshot.id) ?? snapshot.output
|
|
1012
1099
|
: snapshot.output;
|
|
1013
1100
|
if (req.query.format === "chat") {
|
|
1014
|
-
// 客户端带 blockBudget(iOS)走块级窗口:只回最近 N 个块(必要时切掉最旧 turn 的头部),
|
|
1015
|
-
// 根治「单条 turn 上百块/1MB
|
|
1101
|
+
// 客户端带 blockBudget(Web/iOS)走块级窗口:只回最近 N 个块(必要时切掉最旧 turn 的头部),
|
|
1102
|
+
// 根治「单条 turn 上百块/1MB」的长任务打开慢;未启用的客户端保持 turn 级窗口。
|
|
1016
1103
|
const rawBudget = req.query.blockBudget;
|
|
1017
1104
|
if (typeof rawBudget === "string" && /^\d+$/.test(rawBudget) && Number(rawBudget) > 0) {
|
|
1018
1105
|
const blockBudget = parseBoundedInteger(rawBudget, 1, 1, 2_000);
|
|
1019
|
-
const windowed = blockWindowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? []), config.cardDefaults ?? {}, blockBudget);
|
|
1106
|
+
const windowed = blockWindowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? [], snapshot.id), config.cardDefaults ?? {}, blockBudget);
|
|
1020
1107
|
res.json(toSessionDetailDTO(snapshot, {
|
|
1021
1108
|
output: transcriptOutput,
|
|
1022
1109
|
messages: windowed.messages,
|
|
@@ -1028,7 +1115,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1028
1115
|
return;
|
|
1029
1116
|
}
|
|
1030
1117
|
// 与 WS init 对齐:只回最近一窗 turn + offset/total,更早的走 /messages 翻页。
|
|
1031
|
-
const windowed = windowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? []), config.cardDefaults ?? {});
|
|
1118
|
+
const windowed = windowMessagesForTransport(enrichStructuredMessages(snapshot.messages ?? [], snapshot.id), config.cardDefaults ?? {});
|
|
1032
1119
|
res.json(toSessionDetailDTO(snapshot, {
|
|
1033
1120
|
output: transcriptOutput,
|
|
1034
1121
|
messages: windowed.messages,
|
|
@@ -1048,8 +1135,20 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1048
1135
|
res.status(404).json({ error: "未找到该会话,可能已被删除。" });
|
|
1049
1136
|
return;
|
|
1050
1137
|
}
|
|
1051
|
-
const all = enrichStructuredMessages(snapshot.messages ?? []);
|
|
1138
|
+
const all = enrichStructuredMessages(snapshot.messages ?? [], snapshot.id);
|
|
1052
1139
|
const total = all.length;
|
|
1140
|
+
// Web history: fetch at most one block window *before* the currently
|
|
1141
|
+
// displayed turn. A 40-turn page can contain a single multi-MB assistant
|
|
1142
|
+
// turn, defeating the initial block budget entirely.
|
|
1143
|
+
if (typeof req.query.before === "string" && /^\d+$/.test(req.query.before)
|
|
1144
|
+
&& typeof req.query.blockBudget === "string" && /^\d+$/.test(req.query.blockBudget)) {
|
|
1145
|
+
const before = parseBoundedInteger(req.query.before, total, 0, total);
|
|
1146
|
+
const budget = parseBoundedInteger(req.query.blockBudget, 60, 1, 200);
|
|
1147
|
+
const windowed = blockWindowMessagesForTransport(all.slice(0, before), config.cardDefaults ?? {}, budget);
|
|
1148
|
+
res.json({ wandProtocolVersion: WAND_PROTOCOL_VERSION, ...windowed,
|
|
1149
|
+
offset: windowed.messageOffset, total });
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1053
1152
|
// 块级翻页(iOS):?turn=<i>&blockOffset=<当前 leading 偏移>&blockLimit=<N>
|
|
1054
1153
|
// 取该 turn 的 [start, blockOffset) 段(start = max(0, blockOffset - blockLimit))。
|
|
1055
1154
|
const rawTurn = parseInt(String(req.query.turn ?? ""), 10);
|
|
@@ -1069,7 +1168,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1069
1168
|
const blocks = enrichStructuredMessages([{
|
|
1070
1169
|
...turn,
|
|
1071
1170
|
content: sliceTurnBlocksForTransport(turn, blockStart, blockEnd, config.cardDefaults ?? {}),
|
|
1072
|
-
}])[0].content;
|
|
1171
|
+
}], snapshot.id)[0].content;
|
|
1073
1172
|
res.json({ wandProtocolVersion: WAND_PROTOCOL_VERSION, turnIndex, blocks, blockOffset: blockStart, blockTotal });
|
|
1074
1173
|
return;
|
|
1075
1174
|
}
|
|
@@ -1078,7 +1177,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1078
1177
|
const limit = Math.min(Math.max(Number.isFinite(rawLimit) ? rawLimit : 40, 1), 200);
|
|
1079
1178
|
const offset = Math.min(Math.max(Number.isFinite(rawOffset) ? rawOffset : 0, 0), total);
|
|
1080
1179
|
const end = Math.min(offset + limit, total);
|
|
1081
|
-
const slice = enrichStructuredMessages(truncateMessagesForTransport(all.slice(offset, end), config.cardDefaults ?? {}));
|
|
1180
|
+
const slice = enrichStructuredMessages(truncateMessagesForTransport(all.slice(offset, end), config.cardDefaults ?? {}), snapshot.id);
|
|
1082
1181
|
res.json({ wandProtocolVersion: WAND_PROTOCOL_VERSION, messages: slice, offset, total });
|
|
1083
1182
|
});
|
|
1084
1183
|
app.post("/api/sessions/:id/resume", asyncRoute(async (req, res) => {
|
|
@@ -1207,14 +1306,17 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
1207
1306
|
const autoResumeInput = getAutoResumeInitialInput(existingSession, input, view, shortcutKey);
|
|
1208
1307
|
if (autoResumeInput !== null && canAutoResumePtyForInput(existingSession, autoResumeInput)) {
|
|
1209
1308
|
const snapshot = await startResumedPtySession(processes, storage, existingSession, sessionId, defaultMode, {}, autoResumeInput);
|
|
1309
|
+
// Resume input waits for the provider prompt and is deliberately queued after start.
|
|
1310
|
+
// This branch reports a pending delivery, unlike an already-running PTY write.
|
|
1311
|
+
res.setHeader("X-Wand-Input-Delivery", "pending");
|
|
1210
1312
|
if (body.responseMode === "accepted") {
|
|
1211
|
-
res.status(202).json({ accepted: true });
|
|
1313
|
+
res.status(202).json({ accepted: true, deliveryConfirmed: false });
|
|
1212
1314
|
return;
|
|
1213
1315
|
}
|
|
1214
1316
|
res.json(sessionResponseDTO(snapshot));
|
|
1215
1317
|
return;
|
|
1216
1318
|
}
|
|
1217
|
-
const snapshot = processes.
|
|
1319
|
+
const snapshot = await processes.sendInputConfirmed(sessionId, input, view, shortcutKey);
|
|
1218
1320
|
if (body.responseMode === "accepted") {
|
|
1219
1321
|
res.status(202).json({ accepted: true });
|
|
1220
1322
|
return;
|