@co0ontty/wand 4.72.0 → 4.72.1

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.
@@ -1,2 +1,14 @@
1
+ /** Retention view of one structured run, in map insertion order (oldest first). */
2
+ export interface StructuredRunRetentionEntry {
3
+ runId: string;
4
+ status: "running" | "exited";
5
+ exitedAt: number | null;
6
+ logChars: number;
7
+ }
8
+ /**
9
+ * Oldest-first ids of exited runs that fall outside the count, retained-log, or
10
+ * age budget. Running runs are never evicted: their child is still live.
11
+ */
12
+ export declare function structuredRunEvictionOrder(entries: readonly StructuredRunRetentionEntry[], now?: number): string[];
1
13
  export declare function runTerminalDaemon(configPath: string): Promise<void>;
2
14
  export declare function readTerminalDaemonPid(configPath: string): number | null;
@@ -14,6 +14,46 @@ import { PtyTerminalState } from "./pty-terminal-state.js";
14
14
  import { PtyCliExitMarker } from "./pty-shell-launch.js";
15
15
  import { appendWindow, PTY_OUTPUT_MAX_SIZE } from "./pty-text-utils.js";
16
16
  const MAX_STRUCTURED_RUNS = 256;
17
+ /**
18
+ * Exited records exist so a web restart can adopt a finished turn. Each one
19
+ * carries a replay log capped at STRUCTURED_RUN_LOG_MAX_CHARS, so a count-only
20
+ * bound would still let the daemon retain 256 x 8M chars of logs; the exited
21
+ * set is bounded by aggregate log size as well, and old records expire.
22
+ */
23
+ const MAX_EXITED_STRUCTURED_RUNS = 128;
24
+ const EXITED_STRUCTURED_RUN_LOG_MAX_CHARS = 32 * 1024 * 1024;
25
+ const EXITED_STRUCTURED_RUN_RETENTION_MS = 2 * 60 * 60 * 1000;
26
+ /**
27
+ * Oldest-first ids of exited runs that fall outside the count, retained-log, or
28
+ * age budget. Running runs are never evicted: their child is still live.
29
+ */
30
+ export function structuredRunEvictionOrder(entries, now = Date.now()) {
31
+ let total = entries.length;
32
+ let exitedCount = 0;
33
+ let exitedLogChars = 0;
34
+ for (const entry of entries) {
35
+ if (entry.status !== "exited")
36
+ continue;
37
+ exitedCount++;
38
+ exitedLogChars += entry.logChars;
39
+ }
40
+ const evicted = [];
41
+ for (const entry of entries) {
42
+ if (entry.status !== "exited")
43
+ continue;
44
+ const expired = entry.exitedAt !== null && now - entry.exitedAt >= EXITED_STRUCTURED_RUN_RETENTION_MS;
45
+ const overBudget = total > MAX_STRUCTURED_RUNS
46
+ || exitedCount > MAX_EXITED_STRUCTURED_RUNS
47
+ || exitedLogChars > EXITED_STRUCTURED_RUN_LOG_MAX_CHARS;
48
+ if (!expired && !overBudget)
49
+ continue;
50
+ evicted.push(entry.runId);
51
+ total--;
52
+ exitedCount--;
53
+ exitedLogChars -= entry.logChars;
54
+ }
55
+ return evicted;
56
+ }
17
57
  const MAX_REQUEST_BUFFER = 4 * 1024 * 1024;
18
58
  export async function runTerminalDaemon(configPath) {
19
59
  const paths = terminalDaemonPaths(configPath);
@@ -178,16 +218,19 @@ export async function runTerminalDaemon(configPath) {
178
218
  stdoutTruncated: run.stdoutTruncated,
179
219
  stderrTruncated: run.stderrTruncated,
180
220
  });
181
- /** Evict oldest exited records once the map outgrows its bound. */
221
+ /** Evict oldest exited records until counts, retained logs, and age are back in budget. */
182
222
  const evictStaleStructuredRuns = () => {
183
- if (structuredRuns.size <= MAX_STRUCTURED_RUNS)
184
- return;
223
+ const entries = [];
185
224
  for (const [runId, run] of structuredRuns) {
186
- if (structuredRuns.size <= MAX_STRUCTURED_RUNS)
187
- break;
188
- if (run.status === "exited")
189
- structuredRuns.delete(runId);
225
+ entries.push({
226
+ runId,
227
+ status: run.status,
228
+ exitedAt: run.exitedAt,
229
+ logChars: run.stdoutLog.length + run.stderrLog.length,
230
+ });
190
231
  }
232
+ for (const runId of structuredRunEvictionOrder(entries))
233
+ structuredRuns.delete(runId);
191
234
  };
192
235
  const structuredSpawn = (request) => {
193
236
  const existing = structuredRuns.get(request.runId);
@@ -209,6 +252,7 @@ export async function runTerminalDaemon(configPath) {
209
252
  status: "running",
210
253
  exitCode: null,
211
254
  signal: null,
255
+ exitedAt: null,
212
256
  stdoutSeq: 0,
213
257
  stderrSeq: 0,
214
258
  stdoutLog: "",
@@ -252,6 +296,7 @@ export async function runTerminalDaemon(configPath) {
252
296
  run.child = null;
253
297
  run.status = "exited";
254
298
  run.exitCode = code;
299
+ run.exitedAt = Date.now();
255
300
  run.signal = signalName === null || signalName === undefined ? null : Number(signalName) || signalNumberFromName(signalName);
256
301
  broadcast({
257
302
  kind: "event",
@@ -261,6 +306,7 @@ export async function runTerminalDaemon(configPath) {
261
306
  exitCode: run.exitCode ?? undefined,
262
307
  signal: run.signal ?? undefined,
263
308
  });
309
+ evictStaleStructuredRuns();
264
310
  });
265
311
  return serializeStructuredRun(run);
266
312
  };
@@ -15,8 +15,6 @@ export interface TerminalProcess {
15
15
  write(data: string): void;
16
16
  resize(cols: number, rows: number): void;
17
17
  kill(signal?: string): void;
18
- pause(): void;
19
- resume(): void;
20
18
  onData(listener: (event: TerminalDataEvent) => void): {
21
19
  dispose(): void;
22
20
  };
@@ -23,12 +23,6 @@ class InProcessTerminalProcess {
23
23
  kill(signal) {
24
24
  this.child.kill(signal);
25
25
  }
26
- pause() {
27
- this.child.pause();
28
- }
29
- resume() {
30
- this.child.resume();
31
- }
32
26
  onData(listener) {
33
27
  return this.child.onData((data) => listener({ data, seq: ++this.seq }));
34
28
  }