@co0ontty/wand 3.1.1 → 4.0.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/auth.d.ts +19 -5
- package/dist/auth.js +83 -45
- package/dist/build-info.json +3 -3
- package/dist/cert.d.ts +1 -1
- package/dist/cert.js +124 -74
- package/dist/config.js +25 -8
- package/dist/express-async.d.ts +6 -0
- package/dist/express-async.js +28 -0
- package/dist/git-quick-commit.d.ts +2 -0
- package/dist/git-quick-commit.js +215 -76
- package/dist/git-utils.d.ts +4 -0
- package/dist/git-utils.js +60 -11
- package/dist/git-worktree.d.ts +8 -1
- package/dist/git-worktree.js +406 -41
- package/dist/models.d.ts +34 -4
- package/dist/models.js +334 -48
- package/dist/process-manager.d.ts +22 -30
- package/dist/process-manager.js +374 -441
- package/dist/provider-history-scanner.d.ts +54 -0
- package/dist/provider-history-scanner.js +354 -0
- package/dist/request-limits.d.ts +1 -0
- package/dist/request-limits.js +8 -0
- package/dist/resume-policy.d.ts +2 -0
- package/dist/resume-policy.js +5 -0
- package/dist/runtime-config.d.ts +16 -0
- package/dist/runtime-config.js +49 -0
- package/dist/server-file-routes.d.ts +17 -0
- package/dist/server-file-routes.js +653 -0
- package/dist/server-session-routes.d.ts +16 -3
- package/dist/server-session-routes.js +170 -149
- package/dist/server-settings-routes.d.ts +43 -0
- package/dist/server-settings-routes.js +225 -0
- package/dist/server-update-routes.d.ts +61 -0
- package/dist/server-update-routes.js +215 -0
- package/dist/server.d.ts +6 -4
- package/dist/server.js +350 -1313
- package/dist/session-logger.d.ts +32 -2
- package/dist/session-logger.js +145 -15
- package/dist/session-registry.d.ts +27 -0
- package/dist/session-registry.js +153 -0
- package/dist/session-transport.d.ts +31 -0
- package/dist/session-transport.js +82 -0
- package/dist/storage.d.ts +24 -6
- package/dist/storage.js +291 -44
- package/dist/structured-claude-adapter.d.ts +19 -0
- package/dist/structured-claude-adapter.js +117 -0
- package/dist/structured-codex-adapter.d.ts +3 -0
- package/dist/structured-codex-adapter.js +29 -0
- package/dist/structured-opencode-adapter.d.ts +11 -0
- package/dist/structured-opencode-adapter.js +115 -0
- package/dist/structured-provider-common.d.ts +11 -0
- package/dist/structured-provider-common.js +77 -0
- package/dist/structured-session-manager.d.ts +32 -35
- package/dist/structured-session-manager.js +551 -605
- package/dist/types.d.ts +10 -0
- package/dist/update-helper.js +5 -1
- package/dist/web-ui/content/scripts.js +32 -32
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/ws-broadcast.d.ts +16 -1
- package/dist/ws-broadcast.js +124 -58
- package/package.json +2 -1
package/dist/session-logger.d.ts
CHANGED
|
@@ -20,6 +20,15 @@ export interface ShortcutLogContext {
|
|
|
20
20
|
/** Whether the auto-approve was a false positive */
|
|
21
21
|
falsePositive?: boolean;
|
|
22
22
|
}
|
|
23
|
+
/** Optional tuning and I/O injection used by focused logger tests. */
|
|
24
|
+
export interface SessionLoggerOptions {
|
|
25
|
+
flushIntervalMs?: number;
|
|
26
|
+
perFileBufferMaxBytes?: number;
|
|
27
|
+
totalBufferMaxBytes?: number;
|
|
28
|
+
ptyLogMaxBytes?: number;
|
|
29
|
+
ptyLogMaxRotations?: number;
|
|
30
|
+
appendFile?: (filePath: string, data: string) => void;
|
|
31
|
+
}
|
|
23
32
|
/**
|
|
24
33
|
* SessionLogger saves raw session content to local files for debugging and analysis.
|
|
25
34
|
*
|
|
@@ -35,10 +44,20 @@ export interface ShortcutLogContext {
|
|
|
35
44
|
export declare class SessionLogger {
|
|
36
45
|
private readonly baseDir;
|
|
37
46
|
private readonly dirs;
|
|
38
|
-
/** Cached
|
|
47
|
+
/** Cached logical size (disk + pending buffer) so rotation does not stat every chunk. */
|
|
39
48
|
private readonly logSizes;
|
|
40
49
|
private readonly shortcutLogMaxBytes;
|
|
41
|
-
|
|
50
|
+
private readonly flushIntervalMs;
|
|
51
|
+
private readonly perFileBufferMaxBytes;
|
|
52
|
+
private readonly totalBufferMaxBytes;
|
|
53
|
+
private readonly ptyLogMaxBytes;
|
|
54
|
+
private readonly ptyLogMaxRotations;
|
|
55
|
+
private readonly appendFile;
|
|
56
|
+
private readonly appendBuffers;
|
|
57
|
+
private totalBufferedBytes;
|
|
58
|
+
private flushTimer;
|
|
59
|
+
private disposed;
|
|
60
|
+
constructor(configDir: string, shortcutLogMaxBytes?: number, options?: SessionLoggerOptions);
|
|
42
61
|
private ensureDir;
|
|
43
62
|
/**
|
|
44
63
|
* Rotate PTY log files if the current one exceeds the size limit.
|
|
@@ -71,4 +90,15 @@ export declare class SessionLogger {
|
|
|
71
90
|
appendShortcutLog(sessionId: string, shortcutKey: string, tailLines: string, ctx?: ShortcutLogContext): void;
|
|
72
91
|
/** Truncate shortcut log by keeping only the most recent half of entries. Returns the new on-disk size. */
|
|
73
92
|
private truncateShortcutLog;
|
|
93
|
+
/** Synchronously persist all pending append logs owned by one session. */
|
|
94
|
+
flushSession(sessionId: string): void;
|
|
95
|
+
/** Synchronously persist every pending append log owned by this logger. */
|
|
96
|
+
flushAll(): void;
|
|
97
|
+
/** Stop the coalescing timer and synchronously persist the final batch. */
|
|
98
|
+
dispose(): void;
|
|
99
|
+
private enqueueAppend;
|
|
100
|
+
private flushFile;
|
|
101
|
+
private scheduleFlush;
|
|
102
|
+
private clearFlushTimerIfIdle;
|
|
103
|
+
private clearFlushTimer;
|
|
74
104
|
}
|
package/dist/session-logger.js
CHANGED
|
@@ -8,6 +8,12 @@ const PTY_LOG_MAX_SIZE = 50 * 1024 * 1024;
|
|
|
8
8
|
const PTY_LOG_MAX_ROTATIONS = 3;
|
|
9
9
|
/** Default max size for shortcut interaction logs per session (10 MB) */
|
|
10
10
|
const DEFAULT_SHORTCUT_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
/** Delay used to coalesce hot-path append calls without adding noticeable log latency. */
|
|
12
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 40;
|
|
13
|
+
/** Bound memory retained by one hot log file. */
|
|
14
|
+
const DEFAULT_PER_FILE_BUFFER_MAX_BYTES = 256 * 1024;
|
|
15
|
+
/** Bound memory retained across all sessions owned by one logger instance. */
|
|
16
|
+
const DEFAULT_TOTAL_BUFFER_MAX_BYTES = 4 * 1024 * 1024;
|
|
11
17
|
/**
|
|
12
18
|
* SessionLogger saves raw session content to local files for debugging and analysis.
|
|
13
19
|
*
|
|
@@ -23,12 +29,28 @@ const DEFAULT_SHORTCUT_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
|
23
29
|
export class SessionLogger {
|
|
24
30
|
baseDir;
|
|
25
31
|
dirs = new Map();
|
|
26
|
-
/** Cached
|
|
32
|
+
/** Cached logical size (disk + pending buffer) so rotation does not stat every chunk. */
|
|
27
33
|
logSizes = new Map();
|
|
28
34
|
shortcutLogMaxBytes;
|
|
29
|
-
|
|
35
|
+
flushIntervalMs;
|
|
36
|
+
perFileBufferMaxBytes;
|
|
37
|
+
totalBufferMaxBytes;
|
|
38
|
+
ptyLogMaxBytes;
|
|
39
|
+
ptyLogMaxRotations;
|
|
40
|
+
appendFile;
|
|
41
|
+
appendBuffers = new Map();
|
|
42
|
+
totalBufferedBytes = 0;
|
|
43
|
+
flushTimer = null;
|
|
44
|
+
disposed = false;
|
|
45
|
+
constructor(configDir, shortcutLogMaxBytes, options = {}) {
|
|
30
46
|
this.baseDir = path.join(configDir, "sessions");
|
|
31
47
|
this.shortcutLogMaxBytes = shortcutLogMaxBytes ?? DEFAULT_SHORTCUT_LOG_MAX_BYTES;
|
|
48
|
+
this.flushIntervalMs = positiveInteger(options.flushIntervalMs, DEFAULT_FLUSH_INTERVAL_MS);
|
|
49
|
+
this.perFileBufferMaxBytes = positiveInteger(options.perFileBufferMaxBytes, DEFAULT_PER_FILE_BUFFER_MAX_BYTES);
|
|
50
|
+
this.totalBufferMaxBytes = positiveInteger(options.totalBufferMaxBytes, DEFAULT_TOTAL_BUFFER_MAX_BYTES);
|
|
51
|
+
this.ptyLogMaxBytes = positiveInteger(options.ptyLogMaxBytes, PTY_LOG_MAX_SIZE);
|
|
52
|
+
this.ptyLogMaxRotations = positiveInteger(options.ptyLogMaxRotations, PTY_LOG_MAX_ROTATIONS);
|
|
53
|
+
this.appendFile = options.appendFile ?? ((filePath, data) => appendFileSync(filePath, data));
|
|
32
54
|
try {
|
|
33
55
|
mkdirSync(this.baseDir, { recursive: true });
|
|
34
56
|
}
|
|
@@ -62,12 +84,12 @@ export class SessionLogger {
|
|
|
62
84
|
*/
|
|
63
85
|
rotatePtyLog(dir) {
|
|
64
86
|
// Delete oldest if it exists (beyond max rotations)
|
|
65
|
-
const oldest = path.join(dir, `pty-output.log.${
|
|
87
|
+
const oldest = path.join(dir, `pty-output.log.${this.ptyLogMaxRotations}`);
|
|
66
88
|
if (existsSync(oldest)) {
|
|
67
89
|
unlinkSync(oldest);
|
|
68
90
|
}
|
|
69
91
|
// Shift existing rotations up by one
|
|
70
|
-
for (let i =
|
|
92
|
+
for (let i = this.ptyLogMaxRotations - 1; i >= 1; i--) {
|
|
71
93
|
const src = path.join(dir, `pty-output.log.${i}`);
|
|
72
94
|
const dst = path.join(dir, `pty-output.log.${i + 1}`);
|
|
73
95
|
if (existsSync(src)) {
|
|
@@ -82,16 +104,21 @@ export class SessionLogger {
|
|
|
82
104
|
}
|
|
83
105
|
/** Append raw PTY output chunk */
|
|
84
106
|
appendPtyOutput(sessionId, chunk) {
|
|
107
|
+
if (this.disposed || chunk.length === 0)
|
|
108
|
+
return;
|
|
85
109
|
try {
|
|
86
110
|
const dir = this.ensureDir(sessionId);
|
|
87
111
|
const sizes = this.logSizes.get(sessionId);
|
|
88
|
-
|
|
112
|
+
const logPath = path.join(dir, "pty-output.log");
|
|
113
|
+
if (sizes.pty >= this.ptyLogMaxBytes) {
|
|
114
|
+
// Pending bytes must reach the current file before it is renamed.
|
|
115
|
+
this.flushFile(logPath);
|
|
89
116
|
this.rotatePtyLog(dir);
|
|
90
117
|
sizes.pty = 0;
|
|
91
118
|
}
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
const chunkBytes = Buffer.byteLength(chunk);
|
|
120
|
+
sizes.pty += chunkBytes;
|
|
121
|
+
this.enqueueAppend(sessionId, logPath, chunk, chunkBytes, "pty");
|
|
95
122
|
}
|
|
96
123
|
catch {
|
|
97
124
|
// Non-critical — don't let logging failures affect main flow
|
|
@@ -100,9 +127,10 @@ export class SessionLogger {
|
|
|
100
127
|
/** Read the full PTY transcript including rotated logs, oldest first. */
|
|
101
128
|
readPtyOutput(sessionId) {
|
|
102
129
|
try {
|
|
130
|
+
this.flushSession(sessionId);
|
|
103
131
|
const dir = this.ensureDir(sessionId);
|
|
104
132
|
const parts = [];
|
|
105
|
-
for (let index =
|
|
133
|
+
for (let index = this.ptyLogMaxRotations; index >= 1; index -= 1) {
|
|
106
134
|
const rotatedPath = path.join(dir, `pty-output.log.${index}`);
|
|
107
135
|
if (existsSync(rotatedPath)) {
|
|
108
136
|
parts.push(readFileSync(rotatedPath, "utf8"));
|
|
@@ -122,9 +150,11 @@ export class SessionLogger {
|
|
|
122
150
|
}
|
|
123
151
|
/** Append a native mode NDJSON event */
|
|
124
152
|
appendStreamEvent(sessionId, event) {
|
|
153
|
+
if (this.disposed)
|
|
154
|
+
return;
|
|
125
155
|
try {
|
|
126
156
|
const dir = this.ensureDir(sessionId);
|
|
127
|
-
|
|
157
|
+
this.enqueueAppend(sessionId, path.join(dir, "stream-events.jsonl"), JSON.stringify(event) + "\n");
|
|
128
158
|
}
|
|
129
159
|
catch {
|
|
130
160
|
// Non-critical
|
|
@@ -132,9 +162,11 @@ export class SessionLogger {
|
|
|
132
162
|
}
|
|
133
163
|
/** Append raw stdout chunk from a structured-mode child process. */
|
|
134
164
|
appendStructuredStdout(sessionId, chunk) {
|
|
165
|
+
if (this.disposed || chunk.length === 0)
|
|
166
|
+
return;
|
|
135
167
|
try {
|
|
136
168
|
const dir = this.ensureDir(sessionId);
|
|
137
|
-
|
|
169
|
+
this.enqueueAppend(sessionId, path.join(dir, "structured-stdout.log"), chunk);
|
|
138
170
|
}
|
|
139
171
|
catch {
|
|
140
172
|
// Non-critical
|
|
@@ -142,9 +174,11 @@ export class SessionLogger {
|
|
|
142
174
|
}
|
|
143
175
|
/** Append raw stderr chunk from a structured-mode child process. */
|
|
144
176
|
appendStructuredStderr(sessionId, chunk) {
|
|
177
|
+
if (this.disposed || chunk.length === 0)
|
|
178
|
+
return;
|
|
145
179
|
try {
|
|
146
180
|
const dir = this.ensureDir(sessionId);
|
|
147
|
-
|
|
181
|
+
this.enqueueAppend(sessionId, path.join(dir, "structured-stderr.log"), chunk);
|
|
148
182
|
}
|
|
149
183
|
catch {
|
|
150
184
|
// Non-critical
|
|
@@ -152,10 +186,12 @@ export class SessionLogger {
|
|
|
152
186
|
}
|
|
153
187
|
/** Append a spawn metadata record (args, pid, cwd, exit, errors, …) for a structured run. */
|
|
154
188
|
appendStructuredSpawn(sessionId, meta) {
|
|
189
|
+
if (this.disposed)
|
|
190
|
+
return;
|
|
155
191
|
try {
|
|
156
192
|
const dir = this.ensureDir(sessionId);
|
|
157
193
|
const entry = JSON.stringify({ ts: new Date().toISOString(), ...meta }) + "\n";
|
|
158
|
-
|
|
194
|
+
this.enqueueAppend(sessionId, path.join(dir, "structured-spawns.jsonl"), entry);
|
|
159
195
|
}
|
|
160
196
|
catch {
|
|
161
197
|
// Non-critical
|
|
@@ -164,6 +200,7 @@ export class SessionLogger {
|
|
|
164
200
|
/** Read recent stderr tail (for surfacing in failure messages). */
|
|
165
201
|
readStructuredStderrTail(sessionId, maxBytes = 4096) {
|
|
166
202
|
try {
|
|
203
|
+
this.flushSession(sessionId);
|
|
167
204
|
const dir = this.ensureDir(sessionId);
|
|
168
205
|
const filePath = path.join(dir, "structured-stderr.log");
|
|
169
206
|
if (!existsSync(filePath))
|
|
@@ -177,6 +214,8 @@ export class SessionLogger {
|
|
|
177
214
|
}
|
|
178
215
|
/** Save the current structured messages snapshot */
|
|
179
216
|
saveMessages(sessionId, messages) {
|
|
217
|
+
if (this.disposed)
|
|
218
|
+
return;
|
|
180
219
|
try {
|
|
181
220
|
const dir = this.ensureDir(sessionId);
|
|
182
221
|
writeFileSync(path.join(dir, "messages.json"), JSON.stringify(messages, null, 2) + "\n");
|
|
@@ -187,6 +226,8 @@ export class SessionLogger {
|
|
|
187
226
|
}
|
|
188
227
|
/** Save session metadata */
|
|
189
228
|
saveMetadata(sessionId, meta) {
|
|
229
|
+
if (this.disposed)
|
|
230
|
+
return;
|
|
190
231
|
try {
|
|
191
232
|
const dir = this.ensureDir(sessionId);
|
|
192
233
|
writeFileSync(path.join(dir, "metadata.json"), JSON.stringify(meta, null, 2) + "\n");
|
|
@@ -197,6 +238,9 @@ export class SessionLogger {
|
|
|
197
238
|
}
|
|
198
239
|
/** Delete all log files for a session */
|
|
199
240
|
deleteSession(sessionId) {
|
|
241
|
+
// Flush and remove every pending entry before deleting the directory so a
|
|
242
|
+
// later timer cannot recreate files for a deleted session.
|
|
243
|
+
this.flushSession(sessionId);
|
|
200
244
|
const dir = path.join(this.baseDir, sessionId);
|
|
201
245
|
try {
|
|
202
246
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -209,7 +253,7 @@ export class SessionLogger {
|
|
|
209
253
|
}
|
|
210
254
|
/** Append a shortcut key interaction log entry (for analyzing auto-confirm gaps) */
|
|
211
255
|
appendShortcutLog(sessionId, shortcutKey, tailLines, ctx) {
|
|
212
|
-
if (this.shortcutLogMaxBytes <= 0)
|
|
256
|
+
if (this.disposed || this.shortcutLogMaxBytes <= 0)
|
|
213
257
|
return;
|
|
214
258
|
try {
|
|
215
259
|
const dir = this.ensureDir(sessionId);
|
|
@@ -226,10 +270,11 @@ export class SessionLogger {
|
|
|
226
270
|
}) + "\n";
|
|
227
271
|
const entryBytes = Buffer.byteLength(entry);
|
|
228
272
|
if (sizes.shortcut + entryBytes > this.shortcutLogMaxBytes) {
|
|
273
|
+
this.flushFile(logPath);
|
|
229
274
|
sizes.shortcut = this.truncateShortcutLog(logPath);
|
|
230
275
|
}
|
|
231
|
-
appendFileSync(logPath, entry);
|
|
232
276
|
sizes.shortcut += entryBytes;
|
|
277
|
+
this.enqueueAppend(sessionId, logPath, entry, entryBytes, "shortcut");
|
|
233
278
|
}
|
|
234
279
|
catch {
|
|
235
280
|
// Non-critical
|
|
@@ -253,6 +298,86 @@ export class SessionLogger {
|
|
|
253
298
|
return 0;
|
|
254
299
|
}
|
|
255
300
|
}
|
|
301
|
+
/** Synchronously persist all pending append logs owned by one session. */
|
|
302
|
+
flushSession(sessionId) {
|
|
303
|
+
for (const [filePath, pending] of Array.from(this.appendBuffers.entries())) {
|
|
304
|
+
if (pending.sessionId === sessionId)
|
|
305
|
+
this.flushFile(filePath);
|
|
306
|
+
}
|
|
307
|
+
this.clearFlushTimerIfIdle();
|
|
308
|
+
}
|
|
309
|
+
/** Synchronously persist every pending append log owned by this logger. */
|
|
310
|
+
flushAll() {
|
|
311
|
+
this.clearFlushTimer();
|
|
312
|
+
for (const filePath of Array.from(this.appendBuffers.keys())) {
|
|
313
|
+
this.flushFile(filePath);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/** Stop the coalescing timer and synchronously persist the final batch. */
|
|
317
|
+
dispose() {
|
|
318
|
+
if (this.disposed)
|
|
319
|
+
return;
|
|
320
|
+
this.disposed = true;
|
|
321
|
+
this.flushAll();
|
|
322
|
+
}
|
|
323
|
+
enqueueAppend(sessionId, filePath, chunk, byteLength = Buffer.byteLength(chunk), sizeKind) {
|
|
324
|
+
let pending = this.appendBuffers.get(filePath);
|
|
325
|
+
if (!pending) {
|
|
326
|
+
pending = { sessionId, chunks: [], byteLength: 0, sizeKind };
|
|
327
|
+
this.appendBuffers.set(filePath, pending);
|
|
328
|
+
}
|
|
329
|
+
pending.chunks.push(chunk);
|
|
330
|
+
pending.byteLength += byteLength;
|
|
331
|
+
this.totalBufferedBytes += byteLength;
|
|
332
|
+
if (pending.byteLength >= this.perFileBufferMaxBytes) {
|
|
333
|
+
this.flushFile(filePath);
|
|
334
|
+
}
|
|
335
|
+
if (this.totalBufferedBytes >= this.totalBufferMaxBytes) {
|
|
336
|
+
this.flushAll();
|
|
337
|
+
}
|
|
338
|
+
else if (this.totalBufferedBytes > 0) {
|
|
339
|
+
this.scheduleFlush();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
flushFile(filePath) {
|
|
343
|
+
const pending = this.appendBuffers.get(filePath);
|
|
344
|
+
if (!pending)
|
|
345
|
+
return;
|
|
346
|
+
this.appendBuffers.delete(filePath);
|
|
347
|
+
this.totalBufferedBytes = Math.max(0, this.totalBufferedBytes - pending.byteLength);
|
|
348
|
+
try {
|
|
349
|
+
this.appendFile(filePath, pending.chunks.join(""));
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// Match the previous best-effort behavior. Do not retain a failing batch
|
|
353
|
+
// indefinitely, but remove its bytes from logical rotation accounting.
|
|
354
|
+
if (pending.sizeKind) {
|
|
355
|
+
const sizes = this.logSizes.get(pending.sessionId);
|
|
356
|
+
if (sizes)
|
|
357
|
+
sizes[pending.sizeKind] = Math.max(0, sizes[pending.sizeKind] - pending.byteLength);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
this.clearFlushTimerIfIdle();
|
|
361
|
+
}
|
|
362
|
+
scheduleFlush() {
|
|
363
|
+
if (this.flushTimer || this.disposed)
|
|
364
|
+
return;
|
|
365
|
+
this.flushTimer = setTimeout(() => {
|
|
366
|
+
this.flushTimer = null;
|
|
367
|
+
this.flushAll();
|
|
368
|
+
}, this.flushIntervalMs);
|
|
369
|
+
this.flushTimer.unref?.();
|
|
370
|
+
}
|
|
371
|
+
clearFlushTimerIfIdle() {
|
|
372
|
+
if (this.totalBufferedBytes === 0)
|
|
373
|
+
this.clearFlushTimer();
|
|
374
|
+
}
|
|
375
|
+
clearFlushTimer() {
|
|
376
|
+
if (!this.flushTimer)
|
|
377
|
+
return;
|
|
378
|
+
clearTimeout(this.flushTimer);
|
|
379
|
+
this.flushTimer = null;
|
|
380
|
+
}
|
|
256
381
|
}
|
|
257
382
|
function tryStatSize(filePath) {
|
|
258
383
|
try {
|
|
@@ -262,3 +387,8 @@ function tryStatSize(filePath) {
|
|
|
262
387
|
return 0;
|
|
263
388
|
}
|
|
264
389
|
}
|
|
390
|
+
function positiveInteger(value, fallback) {
|
|
391
|
+
return value !== undefined && Number.isFinite(value) && value > 0
|
|
392
|
+
? Math.floor(value)
|
|
393
|
+
: fallback;
|
|
394
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ProcessManager } from "./process-manager.js";
|
|
2
|
+
import type { StructuredSessionManager } from "./structured-session-manager.js";
|
|
3
|
+
import type { WandStorage } from "./storage.js";
|
|
4
|
+
import type { ExecutionMode, SessionSnapshot } from "./types.js";
|
|
5
|
+
export type SessionOwner = "structured" | "pty" | "storage";
|
|
6
|
+
/**
|
|
7
|
+
* Coordinates session ownership without merging the two runner managers.
|
|
8
|
+
* Live structured state wins over PTY, and both win over a durable fallback.
|
|
9
|
+
*/
|
|
10
|
+
export declare class SessionRegistry {
|
|
11
|
+
private readonly processes;
|
|
12
|
+
private readonly structured;
|
|
13
|
+
private readonly storage;
|
|
14
|
+
constructor(processes: ProcessManager, structured: StructuredSessionManager, storage: WandStorage);
|
|
15
|
+
ownerOf(id: string): SessionOwner | null;
|
|
16
|
+
get(id: string): SessionSnapshot | null;
|
|
17
|
+
getLatest(id: string): SessionSnapshot | null;
|
|
18
|
+
listSlim(): SessionSnapshot[];
|
|
19
|
+
setSessionModel(id: string, model: string | null): SessionSnapshot | null;
|
|
20
|
+
setSessionThinkingEffort(id: string, effort: SessionSnapshot["thinkingEffort"]): SessionSnapshot | null;
|
|
21
|
+
setSessionMode(id: string, mode: ExecutionMode): SessionSnapshot | null;
|
|
22
|
+
setSessionTopic(id: string, title: string, description: string): SessionSnapshot | null;
|
|
23
|
+
updateWorktreeState(id: string, status: SessionSnapshot["worktreeMergeStatus"], info: SessionSnapshot["worktreeMergeInfo"]): SessionSnapshot | null;
|
|
24
|
+
delete(id: string): SessionSnapshot | null;
|
|
25
|
+
deleteWithProviderHistory(id: string): SessionSnapshot | null;
|
|
26
|
+
private updateStored;
|
|
27
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
function slimSnapshot(snapshot) {
|
|
2
|
+
const { output: _output, messages: _messages, ...slim } = snapshot;
|
|
3
|
+
return { ...slim, output: "" };
|
|
4
|
+
}
|
|
5
|
+
function addHiddenProviderSessionId(storage, id) {
|
|
6
|
+
const raw = storage.getConfigValue("hidden_claude_session_ids");
|
|
7
|
+
let hidden;
|
|
8
|
+
try {
|
|
9
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
10
|
+
hidden = new Set(Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : []);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
hidden = new Set();
|
|
14
|
+
}
|
|
15
|
+
if (hidden.has(id))
|
|
16
|
+
return;
|
|
17
|
+
hidden.add(id);
|
|
18
|
+
storage.setConfigValue("hidden_claude_session_ids", JSON.stringify(Array.from(hidden)));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Coordinates session ownership without merging the two runner managers.
|
|
22
|
+
* Live structured state wins over PTY, and both win over a durable fallback.
|
|
23
|
+
*/
|
|
24
|
+
export class SessionRegistry {
|
|
25
|
+
processes;
|
|
26
|
+
structured;
|
|
27
|
+
storage;
|
|
28
|
+
constructor(processes, structured, storage) {
|
|
29
|
+
this.processes = processes;
|
|
30
|
+
this.structured = structured;
|
|
31
|
+
this.storage = storage;
|
|
32
|
+
}
|
|
33
|
+
ownerOf(id) {
|
|
34
|
+
if (this.structured.get(id))
|
|
35
|
+
return "structured";
|
|
36
|
+
if (this.processes.getOwned(id))
|
|
37
|
+
return "pty";
|
|
38
|
+
return this.storage.getSession(id) ? "storage" : null;
|
|
39
|
+
}
|
|
40
|
+
get(id) {
|
|
41
|
+
return this.structured.get(id) ?? this.processes.getOwned(id) ?? this.storage.getSession(id);
|
|
42
|
+
}
|
|
43
|
+
getLatest(id) {
|
|
44
|
+
return this.get(id);
|
|
45
|
+
}
|
|
46
|
+
listSlim() {
|
|
47
|
+
const byId = new Map();
|
|
48
|
+
for (const snapshot of this.structured.listSlim())
|
|
49
|
+
byId.set(snapshot.id, snapshot);
|
|
50
|
+
for (const snapshot of this.processes.listSlim()) {
|
|
51
|
+
if (!byId.has(snapshot.id))
|
|
52
|
+
byId.set(snapshot.id, snapshot);
|
|
53
|
+
}
|
|
54
|
+
for (const snapshot of this.storage.loadSessions()) {
|
|
55
|
+
if (!byId.has(snapshot.id))
|
|
56
|
+
byId.set(snapshot.id, slimSnapshot(snapshot));
|
|
57
|
+
}
|
|
58
|
+
return Array.from(byId.values()).sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
59
|
+
}
|
|
60
|
+
setSessionModel(id, model) {
|
|
61
|
+
const owner = this.ownerOf(id);
|
|
62
|
+
if (owner === "structured")
|
|
63
|
+
return this.structured.setSessionModel(id, model);
|
|
64
|
+
if (owner === "pty")
|
|
65
|
+
return this.processes.setSessionModel(id, model);
|
|
66
|
+
return this.updateStored(id, (snapshot) => ({
|
|
67
|
+
...snapshot,
|
|
68
|
+
selectedModel: model?.trim() || null,
|
|
69
|
+
structuredState: (snapshot.sessionKind ?? "pty") === "structured"
|
|
70
|
+
? { ...snapshot.structuredState, model: model?.trim() || undefined }
|
|
71
|
+
: snapshot.structuredState,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
setSessionThinkingEffort(id, effort) {
|
|
75
|
+
const owner = this.ownerOf(id);
|
|
76
|
+
if (owner === "structured")
|
|
77
|
+
return this.structured.setSessionThinkingEffort(id, effort);
|
|
78
|
+
if (owner === "pty")
|
|
79
|
+
return this.processes.setSessionThinkingEffort(id, effort);
|
|
80
|
+
return this.updateStored(id, (snapshot) => ({ ...snapshot, thinkingEffort: effort }));
|
|
81
|
+
}
|
|
82
|
+
setSessionMode(id, mode) {
|
|
83
|
+
const owner = this.ownerOf(id);
|
|
84
|
+
if (owner === "structured")
|
|
85
|
+
return this.structured.setSessionMode(id, mode);
|
|
86
|
+
if (owner === "pty")
|
|
87
|
+
return this.processes.setSessionMode(id, mode);
|
|
88
|
+
return this.updateStored(id, (snapshot) => ({ ...snapshot, mode }));
|
|
89
|
+
}
|
|
90
|
+
setSessionTopic(id, title, description) {
|
|
91
|
+
const owner = this.ownerOf(id);
|
|
92
|
+
if (owner === "structured")
|
|
93
|
+
return this.structured.setSessionTopic(id, title, description);
|
|
94
|
+
if (owner === "pty")
|
|
95
|
+
return this.processes.setSessionTopic(id, title, description);
|
|
96
|
+
return this.updateStored(id, (snapshot) => ({ ...snapshot, title, description, summary: description }));
|
|
97
|
+
}
|
|
98
|
+
updateWorktreeState(id, status, info) {
|
|
99
|
+
const owner = this.ownerOf(id);
|
|
100
|
+
if (owner === "structured")
|
|
101
|
+
return this.structured.setWorktreeMergeState(id, status, info);
|
|
102
|
+
if (owner === "pty")
|
|
103
|
+
return this.processes.setWorktreeMergeState(id, status, info);
|
|
104
|
+
return this.updateStored(id, (snapshot) => ({
|
|
105
|
+
...snapshot,
|
|
106
|
+
worktreeMergeStatus: status,
|
|
107
|
+
worktreeMergeInfo: info ?? null,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
delete(id) {
|
|
111
|
+
const snapshot = this.get(id);
|
|
112
|
+
if (!snapshot)
|
|
113
|
+
return null;
|
|
114
|
+
const owner = this.ownerOf(id);
|
|
115
|
+
if (owner === "structured")
|
|
116
|
+
this.structured.delete(id);
|
|
117
|
+
else if (owner === "pty")
|
|
118
|
+
this.processes.delete(id);
|
|
119
|
+
else
|
|
120
|
+
this.storage.deleteSession(id);
|
|
121
|
+
return snapshot;
|
|
122
|
+
}
|
|
123
|
+
deleteWithProviderHistory(id) {
|
|
124
|
+
const snapshot = this.delete(id);
|
|
125
|
+
const providerSessionId = snapshot?.claudeSessionId?.trim();
|
|
126
|
+
if (!snapshot || !providerSessionId)
|
|
127
|
+
return snapshot;
|
|
128
|
+
const provider = snapshot.provider
|
|
129
|
+
?? snapshot.structuredState?.provider
|
|
130
|
+
?? (/^codex\b/i.test(snapshot.command.trim())
|
|
131
|
+
? "codex"
|
|
132
|
+
: /^opencode\b/i.test(snapshot.command.trim()) ? "opencode" : "claude");
|
|
133
|
+
if (provider === "claude") {
|
|
134
|
+
this.processes.deleteClaudeHistoryFiles([{ claudeSessionId: providerSessionId, cwd: snapshot.cwd }]);
|
|
135
|
+
}
|
|
136
|
+
else if (provider === "codex") {
|
|
137
|
+
this.processes.deleteCodexHistoryFiles([providerSessionId]);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
return snapshot;
|
|
141
|
+
}
|
|
142
|
+
addHiddenProviderSessionId(this.storage, providerSessionId);
|
|
143
|
+
return snapshot;
|
|
144
|
+
}
|
|
145
|
+
updateStored(id, update) {
|
|
146
|
+
const current = this.storage.getSession(id);
|
|
147
|
+
if (!current)
|
|
148
|
+
return null;
|
|
149
|
+
const next = update(current);
|
|
150
|
+
this.storage.updateSessionRuntimeMetadata(next);
|
|
151
|
+
return next;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ConversationTurn, SessionSnapshot } from "./types.js";
|
|
2
|
+
export declare const SESSION_TRANSPORT_OUTPUT_LIMIT = 200000;
|
|
3
|
+
export type SessionBaseDTO = Omit<SessionSnapshot, "output" | "messages">;
|
|
4
|
+
export interface SessionListItemDTO extends SessionBaseDTO {
|
|
5
|
+
/** Kept for compatibility with clients that initialize terminal state from the list. */
|
|
6
|
+
output: "";
|
|
7
|
+
}
|
|
8
|
+
export interface SessionDetailDTO extends SessionBaseDTO {
|
|
9
|
+
output: string;
|
|
10
|
+
outputOffset: number;
|
|
11
|
+
outputTotal: number;
|
|
12
|
+
outputTruncated: boolean;
|
|
13
|
+
messages?: ConversationTurn[];
|
|
14
|
+
messageOffset?: number;
|
|
15
|
+
messageTotal?: number;
|
|
16
|
+
leadingBlockOffset?: number;
|
|
17
|
+
leadingBlockTotal?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function toSessionListItemDTO(snapshot: SessionSnapshot): SessionListItemDTO;
|
|
20
|
+
export interface SessionDetailDTOOptions {
|
|
21
|
+
output?: string;
|
|
22
|
+
messages?: ConversationTurn[];
|
|
23
|
+
messageOffset?: number;
|
|
24
|
+
messageTotal?: number;
|
|
25
|
+
leadingBlockOffset?: number;
|
|
26
|
+
leadingBlockTotal?: number;
|
|
27
|
+
outputLimit?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function toSessionDetailDTO(snapshot: SessionSnapshot, options?: SessionDetailDTOOptions): SessionDetailDTO;
|
|
30
|
+
/** Bound snapshot-like event payloads before they enter per-client WS queues. */
|
|
31
|
+
export declare function boundSessionEventData(data: unknown, outputLimit?: number): unknown;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const SESSION_TRANSPORT_OUTPUT_LIMIT = 200_000;
|
|
2
|
+
/** Explicit allow-list separating the server's session object from its wire DTO. */
|
|
3
|
+
function sessionBase(snapshot) {
|
|
4
|
+
return {
|
|
5
|
+
id: snapshot.id,
|
|
6
|
+
sessionSource: snapshot.sessionSource,
|
|
7
|
+
automationId: snapshot.automationId,
|
|
8
|
+
sessionKind: snapshot.sessionKind,
|
|
9
|
+
provider: snapshot.provider,
|
|
10
|
+
runner: snapshot.runner,
|
|
11
|
+
command: snapshot.command,
|
|
12
|
+
cwd: snapshot.cwd,
|
|
13
|
+
mode: snapshot.mode,
|
|
14
|
+
worktreeEnabled: snapshot.worktreeEnabled,
|
|
15
|
+
worktree: snapshot.worktree,
|
|
16
|
+
worktreeMergeStatus: snapshot.worktreeMergeStatus,
|
|
17
|
+
worktreeMergeInfo: snapshot.worktreeMergeInfo,
|
|
18
|
+
autonomyPolicy: snapshot.autonomyPolicy,
|
|
19
|
+
approvalPolicy: snapshot.approvalPolicy,
|
|
20
|
+
allowedScopes: snapshot.allowedScopes,
|
|
21
|
+
status: snapshot.status,
|
|
22
|
+
exitCode: snapshot.exitCode,
|
|
23
|
+
startedAt: snapshot.startedAt,
|
|
24
|
+
endedAt: snapshot.endedAt,
|
|
25
|
+
archived: snapshot.archived,
|
|
26
|
+
archivedAt: snapshot.archivedAt,
|
|
27
|
+
permissionBlocked: snapshot.permissionBlocked,
|
|
28
|
+
pendingEscalation: snapshot.pendingEscalation,
|
|
29
|
+
lastEscalationResult: snapshot.lastEscalationResult,
|
|
30
|
+
claudeSessionId: snapshot.claudeSessionId,
|
|
31
|
+
queuedMessages: snapshot.queuedMessages,
|
|
32
|
+
structuredState: snapshot.structuredState,
|
|
33
|
+
resumedFromSessionId: snapshot.resumedFromSessionId,
|
|
34
|
+
autoRecovered: snapshot.autoRecovered,
|
|
35
|
+
autoApprovePermissions: snapshot.autoApprovePermissions,
|
|
36
|
+
approvalStats: snapshot.approvalStats,
|
|
37
|
+
summary: snapshot.summary,
|
|
38
|
+
title: snapshot.title,
|
|
39
|
+
description: snapshot.description,
|
|
40
|
+
currentTaskTitle: snapshot.currentTaskTitle,
|
|
41
|
+
selectedModel: snapshot.selectedModel,
|
|
42
|
+
thinkingEffort: snapshot.thinkingEffort,
|
|
43
|
+
ptyCols: snapshot.ptyCols,
|
|
44
|
+
ptyRows: snapshot.ptyRows,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function toSessionListItemDTO(snapshot) {
|
|
48
|
+
return { ...sessionBase(snapshot), output: "" };
|
|
49
|
+
}
|
|
50
|
+
export function toSessionDetailDTO(snapshot, options = {}) {
|
|
51
|
+
const rawOutput = options.output ?? snapshot.output;
|
|
52
|
+
const outputLimit = Math.max(1, options.outputLimit ?? SESSION_TRANSPORT_OUTPUT_LIMIT);
|
|
53
|
+
const outputOffset = Math.max(0, rawOutput.length - outputLimit);
|
|
54
|
+
return {
|
|
55
|
+
...sessionBase(snapshot),
|
|
56
|
+
output: outputOffset > 0 ? rawOutput.slice(outputOffset) : rawOutput,
|
|
57
|
+
outputOffset,
|
|
58
|
+
outputTotal: rawOutput.length,
|
|
59
|
+
outputTruncated: outputOffset > 0,
|
|
60
|
+
...(options.messages !== undefined ? { messages: options.messages } : {}),
|
|
61
|
+
...(options.messageOffset !== undefined ? { messageOffset: options.messageOffset } : {}),
|
|
62
|
+
...(options.messageTotal !== undefined ? { messageTotal: options.messageTotal } : {}),
|
|
63
|
+
...(options.leadingBlockOffset !== undefined ? { leadingBlockOffset: options.leadingBlockOffset } : {}),
|
|
64
|
+
...(options.leadingBlockTotal !== undefined ? { leadingBlockTotal: options.leadingBlockTotal } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Bound snapshot-like event payloads before they enter per-client WS queues. */
|
|
68
|
+
export function boundSessionEventData(data, outputLimit = SESSION_TRANSPORT_OUTPUT_LIMIT) {
|
|
69
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
70
|
+
return data;
|
|
71
|
+
const record = data;
|
|
72
|
+
if (typeof record.output !== "string" || record.output.length <= outputLimit)
|
|
73
|
+
return data;
|
|
74
|
+
const outputOffset = record.output.length - outputLimit;
|
|
75
|
+
return {
|
|
76
|
+
...record,
|
|
77
|
+
output: record.output.slice(outputOffset),
|
|
78
|
+
outputOffset,
|
|
79
|
+
outputTotal: record.output.length,
|
|
80
|
+
outputTruncated: true,
|
|
81
|
+
};
|
|
82
|
+
}
|