@co0ontty/wand 1.20.4 → 1.21.5
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/claude-pty-bridge.d.ts +5 -2
- package/dist/claude-pty-bridge.js +26 -13
- package/dist/config.js +2 -0
- package/dist/git-quick-commit.js +12 -4
- package/dist/models.d.ts +3 -1
- package/dist/models.js +45 -7
- package/dist/process-manager.d.ts +2 -0
- package/dist/process-manager.js +82 -19
- package/dist/pty-text-utils.d.ts +31 -1
- package/dist/pty-text-utils.js +164 -2
- package/dist/server-session-routes.d.ts +1 -1
- package/dist/server-session-routes.js +31 -11
- package/dist/server.d.ts +3 -0
- package/dist/server.js +54 -13
- package/dist/session-logger.d.ts +18 -5
- package/dist/session-logger.js +81 -20
- package/dist/structured-session-manager.d.ts +45 -2
- package/dist/structured-session-manager.js +1010 -35
- package/dist/types.d.ts +15 -2
- package/dist/web-ui/content/scripts.js +785 -238
- package/dist/web-ui/content/styles.css +137 -41
- package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -1
- package/dist/ws-broadcast.d.ts +6 -0
- package/dist/ws-broadcast.js +69 -20
- package/package.json +2 -1
package/dist/session-logger.js
CHANGED
|
@@ -12,14 +12,19 @@ const DEFAULT_SHORTCUT_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
|
12
12
|
* SessionLogger saves raw session content to local files for debugging and analysis.
|
|
13
13
|
*
|
|
14
14
|
* Directory structure: .wand/sessions/{sessionId}/
|
|
15
|
-
* - pty-output.log
|
|
16
|
-
* - pty-output.log.1..3
|
|
17
|
-
* - stream-events.jsonl
|
|
18
|
-
* - messages.json
|
|
15
|
+
* - pty-output.log Raw PTY output (current, rotated when > 50 MB)
|
|
16
|
+
* - pty-output.log.1..3 Rotated PTY output backups
|
|
17
|
+
* - stream-events.jsonl NDJSON events from native mode (append-only)
|
|
18
|
+
* - messages.json Final structured messages (overwritten on each update)
|
|
19
|
+
* - structured-stdout.log Raw stdout from `codex exec` / `claude -p` child (append-only)
|
|
20
|
+
* - structured-stderr.log Raw stderr from the same child (append-only)
|
|
21
|
+
* - structured-spawns.jsonl One line per spawn: args/pid/cwd/exit/error metadata
|
|
19
22
|
*/
|
|
20
23
|
export class SessionLogger {
|
|
21
24
|
baseDir;
|
|
22
25
|
dirs = new Map();
|
|
26
|
+
/** Cached on-disk size of hot-path log files so we can rotate without stat'ing on every chunk. */
|
|
27
|
+
logSizes = new Map();
|
|
23
28
|
shortcutLogMaxBytes;
|
|
24
29
|
constructor(configDir, shortcutLogMaxBytes) {
|
|
25
30
|
this.baseDir = path.join(configDir, "sessions");
|
|
@@ -43,6 +48,10 @@ export class SessionLogger {
|
|
|
43
48
|
// ignore
|
|
44
49
|
}
|
|
45
50
|
this.dirs.set(sessionId, dir);
|
|
51
|
+
// Seed the size cache from disk on first use; subsequent appends maintain
|
|
52
|
+
// the counter in memory so the hot path no longer touches stat/exists.
|
|
53
|
+
const sizes = { pty: tryStatSize(path.join(dir, "pty-output.log")), shortcut: tryStatSize(path.join(dir, "shortcut-interactions.jsonl")) };
|
|
54
|
+
this.logSizes.set(sessionId, sizes);
|
|
46
55
|
return dir;
|
|
47
56
|
}
|
|
48
57
|
/**
|
|
@@ -75,15 +84,14 @@ export class SessionLogger {
|
|
|
75
84
|
appendPtyOutput(sessionId, chunk) {
|
|
76
85
|
try {
|
|
77
86
|
const dir = this.ensureDir(sessionId);
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (stats.size >= PTY_LOG_MAX_SIZE) {
|
|
83
|
-
this.rotatePtyLog(dir);
|
|
84
|
-
}
|
|
87
|
+
const sizes = this.logSizes.get(sessionId);
|
|
88
|
+
if (sizes.pty >= PTY_LOG_MAX_SIZE) {
|
|
89
|
+
this.rotatePtyLog(dir);
|
|
90
|
+
sizes.pty = 0;
|
|
85
91
|
}
|
|
92
|
+
const logPath = path.join(dir, "pty-output.log");
|
|
86
93
|
appendFileSync(logPath, chunk);
|
|
94
|
+
sizes.pty += Buffer.byteLength(chunk);
|
|
87
95
|
}
|
|
88
96
|
catch {
|
|
89
97
|
// Non-critical — don't let logging failures affect main flow
|
|
@@ -122,6 +130,51 @@ export class SessionLogger {
|
|
|
122
130
|
// Non-critical
|
|
123
131
|
}
|
|
124
132
|
}
|
|
133
|
+
/** Append raw stdout chunk from a structured-mode child process. */
|
|
134
|
+
appendStructuredStdout(sessionId, chunk) {
|
|
135
|
+
try {
|
|
136
|
+
const dir = this.ensureDir(sessionId);
|
|
137
|
+
appendFileSync(path.join(dir, "structured-stdout.log"), chunk);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
// Non-critical
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/** Append raw stderr chunk from a structured-mode child process. */
|
|
144
|
+
appendStructuredStderr(sessionId, chunk) {
|
|
145
|
+
try {
|
|
146
|
+
const dir = this.ensureDir(sessionId);
|
|
147
|
+
appendFileSync(path.join(dir, "structured-stderr.log"), chunk);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Non-critical
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Append a spawn metadata record (args, pid, cwd, exit, errors, …) for a structured run. */
|
|
154
|
+
appendStructuredSpawn(sessionId, meta) {
|
|
155
|
+
try {
|
|
156
|
+
const dir = this.ensureDir(sessionId);
|
|
157
|
+
const entry = JSON.stringify({ ts: new Date().toISOString(), ...meta }) + "\n";
|
|
158
|
+
appendFileSync(path.join(dir, "structured-spawns.jsonl"), entry);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Non-critical
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Read recent stderr tail (for surfacing in failure messages). */
|
|
165
|
+
readStructuredStderrTail(sessionId, maxBytes = 4096) {
|
|
166
|
+
try {
|
|
167
|
+
const dir = this.ensureDir(sessionId);
|
|
168
|
+
const filePath = path.join(dir, "structured-stderr.log");
|
|
169
|
+
if (!existsSync(filePath))
|
|
170
|
+
return "";
|
|
171
|
+
const content = readFileSync(filePath, "utf8");
|
|
172
|
+
return content.length <= maxBytes ? content : content.slice(content.length - maxBytes);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return "";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
125
178
|
/** Save the current structured messages snapshot */
|
|
126
179
|
saveMessages(sessionId, messages) {
|
|
127
180
|
try {
|
|
@@ -152,6 +205,7 @@ export class SessionLogger {
|
|
|
152
205
|
// Non-critical
|
|
153
206
|
}
|
|
154
207
|
this.dirs.delete(sessionId);
|
|
208
|
+
this.logSizes.delete(sessionId);
|
|
155
209
|
}
|
|
156
210
|
/** Append a shortcut key interaction log entry (for analyzing auto-confirm gaps) */
|
|
157
211
|
appendShortcutLog(sessionId, shortcutKey, tailLines, ctx) {
|
|
@@ -159,6 +213,7 @@ export class SessionLogger {
|
|
|
159
213
|
return;
|
|
160
214
|
try {
|
|
161
215
|
const dir = this.ensureDir(sessionId);
|
|
216
|
+
const sizes = this.logSizes.get(sessionId);
|
|
162
217
|
const logPath = path.join(dir, "shortcut-interactions.jsonl");
|
|
163
218
|
const entry = JSON.stringify({
|
|
164
219
|
ts: new Date().toISOString(),
|
|
@@ -169,35 +224,41 @@ export class SessionLogger {
|
|
|
169
224
|
input: ctx?.input,
|
|
170
225
|
tail: tailLines,
|
|
171
226
|
}) + "\n";
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
if (size + entry.length > this.shortcutLogMaxBytes) {
|
|
176
|
-
this.truncateShortcutLog(logPath);
|
|
177
|
-
}
|
|
227
|
+
const entryBytes = Buffer.byteLength(entry);
|
|
228
|
+
if (sizes.shortcut + entryBytes > this.shortcutLogMaxBytes) {
|
|
229
|
+
sizes.shortcut = this.truncateShortcutLog(logPath);
|
|
178
230
|
}
|
|
179
231
|
appendFileSync(logPath, entry);
|
|
232
|
+
sizes.shortcut += entryBytes;
|
|
180
233
|
}
|
|
181
234
|
catch {
|
|
182
235
|
// Non-critical
|
|
183
236
|
}
|
|
184
237
|
}
|
|
185
|
-
/** Truncate shortcut log by keeping only the most recent half of entries */
|
|
238
|
+
/** Truncate shortcut log by keeping only the most recent half of entries. Returns the new on-disk size. */
|
|
186
239
|
truncateShortcutLog(logPath) {
|
|
187
240
|
try {
|
|
188
241
|
const content = readFileSync(logPath, "utf8");
|
|
189
242
|
const lines = content.split("\n").filter(Boolean);
|
|
190
|
-
// Keep the latter half
|
|
191
243
|
const keepFrom = Math.floor(lines.length / 2);
|
|
192
244
|
const trimmed = lines.slice(keepFrom).join("\n") + "\n";
|
|
193
245
|
writeFileSync(logPath, trimmed);
|
|
246
|
+
return Buffer.byteLength(trimmed);
|
|
194
247
|
}
|
|
195
248
|
catch {
|
|
196
|
-
// If truncation fails, delete the file to prevent unbounded growth
|
|
197
249
|
try {
|
|
198
250
|
unlinkSync(logPath);
|
|
199
251
|
}
|
|
200
252
|
catch { /* ignore */ }
|
|
253
|
+
return 0;
|
|
201
254
|
}
|
|
202
255
|
}
|
|
203
256
|
}
|
|
257
|
+
function tryStatSize(filePath) {
|
|
258
|
+
try {
|
|
259
|
+
return existsSync(filePath) ? statSync(filePath).size : 0;
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { SessionLogger } from "./session-logger.js";
|
|
1
2
|
import { WandStorage } from "./storage.js";
|
|
2
|
-
import { ExecutionMode, ProcessEvent, SessionRunner, SessionSnapshot, WandConfig } from "./types.js";
|
|
3
|
+
import { ExecutionMode, ProcessEvent, SessionProvider, SessionRunner, SessionSnapshot, WandConfig } from "./types.js";
|
|
3
4
|
interface CreateStructuredSessionOptions {
|
|
4
5
|
cwd: string;
|
|
5
6
|
mode: ExecutionMode;
|
|
6
7
|
prompt?: string;
|
|
8
|
+
provider?: SessionProvider;
|
|
7
9
|
runner?: SessionRunner;
|
|
8
10
|
worktreeEnabled?: boolean;
|
|
9
11
|
/** 用户指定的 Claude 模型(别名或完整 ID)。留空则 spawn 时不加 --model。 */
|
|
@@ -12,14 +14,33 @@ interface CreateStructuredSessionOptions {
|
|
|
12
14
|
export declare class StructuredSessionManager {
|
|
13
15
|
private readonly storage;
|
|
14
16
|
private readonly config;
|
|
17
|
+
private readonly logger;
|
|
15
18
|
private readonly sessions;
|
|
16
19
|
private readonly pendingChildren;
|
|
20
|
+
private readonly pendingSdkAbort;
|
|
17
21
|
private readonly interruptedWith;
|
|
22
|
+
/** Last wall-clock time (ms) we did a full saveSession for a streaming session. */
|
|
23
|
+
private readonly lastStreamSaveAt;
|
|
24
|
+
/**
|
|
25
|
+
* Idempotency keys we've already accepted, mapped to their wall-clock timestamp.
|
|
26
|
+
* Android WebView 在进程恢复时偶尔会重发上一个未收到响应的 POST(HTTP/2 stream
|
|
27
|
+
* reset 等场景),客户端 JS 没有重试逻辑也拦不住。这里用 (sessionId, key) 永
|
|
28
|
+
* 久去重,重复就抛错让前端弹 toast 提示,**不**做任何处理。timestamp 仅用于
|
|
29
|
+
* map 大小溢出时按时间裁剪。
|
|
30
|
+
*/
|
|
31
|
+
private readonly seenIdempotencyKeys;
|
|
18
32
|
private emitEvent;
|
|
19
33
|
private archiveTimer;
|
|
20
|
-
constructor(storage: WandStorage, config: WandConfig);
|
|
34
|
+
constructor(storage: WandStorage, config: WandConfig, logger?: SessionLogger | null);
|
|
21
35
|
private archiveExpiredSessions;
|
|
22
36
|
setEventEmitter(emitEvent: (event: ProcessEvent) => void): void;
|
|
37
|
+
/**
|
|
38
|
+
* In-memory snapshot is updated unconditionally; the SQLite write is rate-
|
|
39
|
+
* limited to once per STREAM_SAVE_THROTTLE_MS. Caller must still invoke
|
|
40
|
+
* `storage.saveSession` directly at terminal events (close / failure) so the
|
|
41
|
+
* final state is durable.
|
|
42
|
+
*/
|
|
43
|
+
private saveStreamingSnapshot;
|
|
23
44
|
list(): SessionSnapshot[];
|
|
24
45
|
/** Return lightweight snapshots for the session list (no output/messages). */
|
|
25
46
|
listSlim(): SessionSnapshot[];
|
|
@@ -27,6 +48,7 @@ export declare class StructuredSessionManager {
|
|
|
27
48
|
createSession(options: CreateStructuredSessionOptions): SessionSnapshot;
|
|
28
49
|
sendMessage(id: string, input: string, opts?: {
|
|
29
50
|
interrupt?: boolean;
|
|
51
|
+
idempotencyKey?: string;
|
|
30
52
|
}): Promise<SessionSnapshot>;
|
|
31
53
|
/** Approve a pending permission request. */
|
|
32
54
|
approvePermission(sessionId: string): SessionSnapshot;
|
|
@@ -49,6 +71,8 @@ export declare class StructuredSessionManager {
|
|
|
49
71
|
private resolvePermission;
|
|
50
72
|
private incrementApprovalStats;
|
|
51
73
|
private buildPermissionArgs;
|
|
74
|
+
private buildCodexArgs;
|
|
75
|
+
private runCodexStreaming;
|
|
52
76
|
/**
|
|
53
77
|
* Spawn `claude -p --output-format stream-json` and parse NDJSON lines as
|
|
54
78
|
* they arrive, emitting incremental WebSocket events so the UI can render
|
|
@@ -61,11 +85,30 @@ export declare class StructuredSessionManager {
|
|
|
61
85
|
* outside CWD). stdin is always "ignore" — no ACP bidirectional control.
|
|
62
86
|
*/
|
|
63
87
|
private runClaudeStreaming;
|
|
88
|
+
/**
|
|
89
|
+
* Use @anthropic-ai/claude-agent-sdk instead of spawning claude -p directly.
|
|
90
|
+
* The SDK still spawns the claude binary but provides typed AsyncGenerator<SDKMessage>
|
|
91
|
+
* messages, so we skip NDJSON parsing. Options are 1:1 with the CLI flags.
|
|
92
|
+
*
|
|
93
|
+
* Streaming is enabled via includePartialMessages: true — the SDK emits
|
|
94
|
+
* SDKPartialAssistantMessage (type: "stream_event") with BetaRawMessageStreamEvent
|
|
95
|
+
* payloads for incremental text/thinking/tool_use updates, followed by a final
|
|
96
|
+
* SDKAssistantMessage with the authoritative complete content.
|
|
97
|
+
*/
|
|
98
|
+
private runClaudeSdkStreaming;
|
|
99
|
+
private _runClaudeSdkStreamingAsync;
|
|
64
100
|
private extractAssistantMessage;
|
|
65
101
|
private compactContentBlocks;
|
|
66
102
|
private normalizeToolInput;
|
|
67
103
|
private normalizeToolResultContent;
|
|
104
|
+
private extractCodexText;
|
|
105
|
+
private extractCodexItemBlock;
|
|
106
|
+
private upsertCodexBlock;
|
|
107
|
+
private finishStructuredFailure;
|
|
68
108
|
private extractModelName;
|
|
69
109
|
private extractUsage;
|
|
110
|
+
/** Extract usage from an SDKResultSuccess message (sdk runner). */
|
|
111
|
+
private extractSdkUsage;
|
|
112
|
+
private extractCodexUsage;
|
|
70
113
|
}
|
|
71
114
|
export {};
|