@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.
@@ -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 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)
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 logPath = path.join(dir, "pty-output.log");
79
- // Check size and rotate if needed
80
- if (existsSync(logPath)) {
81
- const stats = statSync(logPath);
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
- // Check size and truncate if needed
173
- if (existsSync(logPath)) {
174
- const size = statSync(logPath).size;
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 {};