@oh-my-pi/pi-coding-agent 3.5.1337 → 3.8.1337

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/CHANGELOG.md CHANGED
@@ -2,6 +2,35 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [3.8.1337] - 2026-01-04
6
+ ### Added
7
+
8
+ - Added automatic browser opening after exporting session to HTML
9
+ - Added automatic browser opening after sharing session as a Gist
10
+
11
+ ### Fixed
12
+
13
+ - Fixed session titles not persisting to file when set before first flush
14
+
15
+ ## [3.7.1337] - 2026-01-04
16
+ ### Added
17
+
18
+ - Added `EditMatchError` class for structured error handling in edit operations
19
+ - Added `utils` module export with `once` and `untilAborted` helper functions
20
+ - Added in-memory LSP content sync via `syncContent` and `notifySaved` client methods
21
+
22
+ ### Changed
23
+
24
+ - Refactored LSP integration to use writethrough callbacks for edit and write tools, improving performance by syncing content in-memory before disk writes
25
+ - Simplified FileDiagnosticsResult interface with renamed fields: `diagnostics` → `messages`, `hasErrors` → `errored`, `serverName` → `server`
26
+ - Session title generation now triggers before sending the first message rather than after agent work begins
27
+
28
+ ### Fixed
29
+
30
+ - Fixed potential text decoding issues in bash executor by using streaming TextDecoder instead of Buffer.toString()
31
+
32
+ ## [3.6.1337] - 2026-01-03
33
+
5
34
  ## [3.5.1337] - 2026-01-03
6
35
 
7
36
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "3.5.1337",
3
+ "version": "3.8.1337",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -39,9 +39,9 @@
39
39
  "prepublishOnly": "bun run generate-template && bun run clean && bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-agent-core": "3.5.1337",
43
- "@oh-my-pi/pi-ai": "3.5.1337",
44
- "@oh-my-pi/pi-tui": "3.5.1337",
42
+ "@oh-my-pi/pi-agent-core": "3.8.1337",
43
+ "@oh-my-pi/pi-ai": "3.8.1337",
44
+ "@oh-my-pi/pi-tui": "3.8.1337",
45
45
  "@sinclair/typebox": "^0.34.46",
46
46
  "ajv": "^8.17.1",
47
47
  "chalk": "^5.5.0",
@@ -52,6 +52,7 @@
52
52
  "highlight.js": "^11.11.1",
53
53
  "marked": "^15.0.12",
54
54
  "minimatch": "^10.1.1",
55
+ "nanoid": "^5.1.6",
55
56
  "node-html-parser": "^6.1.13",
56
57
  "smol-toml": "^1.6.0",
57
58
  "strip-ansi": "^7.1.2",
@@ -14,6 +14,7 @@ import stripAnsi from "strip-ansi";
14
14
  import { getShellConfig, killProcessTree, sanitizeBinaryOutput } from "../utils/shell";
15
15
  import { getOrCreateSnapshot, getSnapshotSourceCommand } from "../utils/shell-snapshot";
16
16
  import { DEFAULT_MAX_BYTES, truncateTail } from "./tools/truncate";
17
+ import { ScopeSignal } from "./utils";
17
18
 
18
19
  // ============================================================================
19
20
  // Types
@@ -47,6 +48,70 @@ export interface BashResult {
47
48
  // Implementation
48
49
  // ============================================================================
49
50
 
51
+ function createSanitizer(): TransformStream<Uint8Array, string> {
52
+ const decoder = new TextDecoder();
53
+ return new TransformStream({
54
+ transform(chunk, controller) {
55
+ const text = sanitizeBinaryOutput(stripAnsi(decoder.decode(chunk, { stream: true }))).replace(/\r/g, "");
56
+ controller.enqueue(text);
57
+ },
58
+ });
59
+ }
60
+
61
+ function createOutputSink(
62
+ spillThreshold: number,
63
+ maxBuffer: number,
64
+ onChunk?: (text: string) => void,
65
+ ): WritableStream<string> & {
66
+ dump: (annotation?: string) => { output: string; truncated: boolean; fullOutputPath?: string };
67
+ } {
68
+ const chunks: string[] = [];
69
+ let chunkBytes = 0;
70
+ let totalBytes = 0;
71
+ let fullOutputPath: string | undefined;
72
+ let fullOutputStream: WriteStream | undefined;
73
+
74
+ const sink = new WritableStream<string>({
75
+ write(text) {
76
+ totalBytes += text.length;
77
+
78
+ // Spill to temp file if needed
79
+ if (totalBytes > spillThreshold && !fullOutputPath) {
80
+ fullOutputPath = join(tmpdir(), `omp-${crypto.randomUUID()}.buffer`);
81
+ const ts = createWriteStream(fullOutputPath);
82
+ chunks.forEach((c) => {
83
+ ts.write(c);
84
+ });
85
+ fullOutputStream = ts;
86
+ }
87
+ fullOutputStream?.write(text);
88
+
89
+ // Rolling buffer
90
+ chunks.push(text);
91
+ chunkBytes += text.length;
92
+ while (chunkBytes > maxBuffer && chunks.length > 1) {
93
+ chunkBytes -= chunks.shift()!.length;
94
+ }
95
+
96
+ onChunk?.(text);
97
+ },
98
+ close() {
99
+ fullOutputStream?.end();
100
+ },
101
+ });
102
+
103
+ return Object.assign(sink, {
104
+ dump(annotation?: string) {
105
+ if (annotation) {
106
+ chunks.push(`\n\n${annotation}`);
107
+ }
108
+ const full = chunks.join("");
109
+ const { content, truncated } = truncateTail(full);
110
+ return { output: truncated ? content : full, truncated, fullOutputPath: fullOutputPath };
111
+ },
112
+ });
113
+ }
114
+
50
115
  /**
51
116
  * Execute a bash command with optional streaming and cancellation support.
52
117
  *
@@ -72,165 +137,61 @@ export async function executeBash(command: string, options?: BashExecutorOptions
72
137
  const prefixedCommand = prefix ? `${prefix} ${command}` : command;
73
138
  const finalCommand = `${snapshotPrefix}${prefixedCommand}`;
74
139
 
75
- return new Promise((resolve, reject) => {
76
- const child: Subprocess = Bun.spawn([shell, ...args, finalCommand], {
77
- cwd: options?.cwd,
78
- stdin: "ignore",
79
- stdout: "pipe",
80
- stderr: "pipe",
81
- env,
82
- });
83
-
84
- // Track sanitized output for truncation
85
- const outputChunks: string[] = [];
86
- let outputBytes = 0;
87
- const maxOutputBytes = DEFAULT_MAX_BYTES * 2;
88
-
89
- // Temp file for large output
90
- let tempFilePath: string | undefined;
91
- let tempFileStream: WriteStream | undefined;
92
- let totalBytes = 0;
93
- let timedOut = false;
94
-
95
- // Handle abort signal and timeout
96
- const abortHandler = () => {
97
- killProcessTree(child.pid);
98
- };
99
-
100
- // Set up timeout if specified
101
- let timeoutHandle: Timer | undefined;
102
- if (options?.timeout && options.timeout > 0) {
103
- timeoutHandle = setTimeout(() => {
104
- timedOut = true;
105
- abortHandler();
106
- }, options.timeout);
107
- }
140
+ using signal = new ScopeSignal(options);
108
141
 
109
- if (options?.signal) {
110
- if (options.signal.aborted) {
111
- // Already aborted, don't even start
112
- child.kill();
113
- if (timeoutHandle) clearTimeout(timeoutHandle);
114
- resolve({
115
- output: "",
116
- exitCode: undefined,
117
- cancelled: true,
118
- truncated: false,
119
- });
120
- return;
121
- }
122
- options.signal.addEventListener("abort", abortHandler, { once: true });
123
- }
124
-
125
- const handleData = (data: Buffer) => {
126
- totalBytes += data.length;
127
-
128
- // Sanitize once at the source: strip ANSI, replace binary garbage, normalize newlines
129
- const text = sanitizeBinaryOutput(stripAnsi(data.toString())).replace(/\r/g, "");
130
-
131
- // Start writing to temp file if exceeds threshold
132
- if (totalBytes > DEFAULT_MAX_BYTES && !tempFilePath) {
133
- const randomId = crypto.getRandomValues(new Uint8Array(8));
134
- const id = Array.from(randomId, (b) => b.toString(16).padStart(2, "0")).join("");
135
- tempFilePath = join(tmpdir(), `omp-bash-${id}.log`);
136
- tempFileStream = createWriteStream(tempFilePath);
137
- // Write already-buffered chunks to temp file
138
- for (const chunk of outputChunks) {
139
- tempFileStream.write(chunk);
140
- }
141
- }
142
+ const child: Subprocess = Bun.spawn([shell, ...args, finalCommand], {
143
+ cwd: options?.cwd,
144
+ stdin: "ignore",
145
+ stdout: "pipe",
146
+ stderr: "pipe",
147
+ env,
148
+ });
142
149
 
143
- if (tempFileStream) {
144
- tempFileStream.write(text);
145
- }
150
+ signal.catch(() => {
151
+ killProcessTree(child.pid);
152
+ });
146
153
 
147
- // Keep rolling buffer of sanitized text
148
- outputChunks.push(text);
149
- outputBytes += text.length;
150
- while (outputBytes > maxOutputBytes && outputChunks.length > 1) {
151
- const removed = outputChunks.shift()!;
152
- outputBytes -= removed.length;
153
- }
154
+ const sink = createOutputSink(DEFAULT_MAX_BYTES, DEFAULT_MAX_BYTES * 2, options?.onChunk);
154
155
 
155
- // Stream to callback if provided
156
- if (options?.onChunk) {
157
- options.onChunk(text);
158
- }
159
- };
160
-
161
- // Read streams asynchronously
162
- (async () => {
156
+ const writer = sink.getWriter();
157
+ try {
158
+ async function pumpStream(readable: ReadableStream<Uint8Array>) {
159
+ const reader = readable.pipeThrough(createSanitizer()).getReader();
163
160
  try {
164
- const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
165
- const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
166
-
167
- await Promise.all([
168
- (async () => {
169
- while (true) {
170
- const { done, value } = await stdoutReader.read();
171
- if (done) break;
172
- handleData(Buffer.from(value));
173
- }
174
- })(),
175
- (async () => {
176
- while (true) {
177
- const { done, value } = await stderrReader.read();
178
- if (done) break;
179
- handleData(Buffer.from(value));
180
- }
181
- })(),
182
- ]);
183
-
184
- const exitCode = await child.exited;
185
-
186
- // Clean up
187
- if (timeoutHandle) clearTimeout(timeoutHandle);
188
- if (options?.signal) {
189
- options.signal.removeEventListener("abort", abortHandler);
190
- }
191
- if (tempFileStream) {
192
- tempFileStream.end();
193
- }
194
-
195
- // Combine buffered chunks for truncation (already sanitized)
196
- const fullOutput = outputChunks.join("");
197
- const truncationResult = truncateTail(fullOutput);
198
-
199
- // Handle timeout
200
- if (timedOut) {
201
- const timeoutSecs = Math.round((options?.timeout || 0) / 1000);
202
- resolve({
203
- output: `${fullOutput}\n\nCommand timed out after ${timeoutSecs} seconds`,
204
- exitCode: undefined,
205
- cancelled: true,
206
- truncated: truncationResult.truncated,
207
- fullOutputPath: tempFilePath,
208
- });
209
- return;
161
+ while (true) {
162
+ const { done, value } = await reader.read();
163
+ if (done) break;
164
+ await writer.write(value);
210
165
  }
211
-
212
- // Non-zero exit codes or signal-killed processes are considered cancelled if killed via signal
213
- const cancelled = exitCode === null || (exitCode !== 0 && (options?.signal?.aborted ?? false));
214
-
215
- resolve({
216
- output: truncationResult.truncated ? truncationResult.content : fullOutput,
217
- exitCode: cancelled ? undefined : exitCode,
218
- cancelled,
219
- truncated: truncationResult.truncated,
220
- fullOutputPath: tempFilePath,
221
- });
222
- } catch (err) {
223
- // Clean up
224
- if (timeoutHandle) clearTimeout(timeoutHandle);
225
- if (options?.signal) {
226
- options.signal.removeEventListener("abort", abortHandler);
227
- }
228
- if (tempFileStream) {
229
- tempFileStream.end();
230
- }
231
-
232
- reject(err);
166
+ } finally {
167
+ reader.releaseLock();
233
168
  }
234
- })();
235
- });
169
+ }
170
+ await Promise.all([
171
+ pumpStream(child.stdout as ReadableStream<Uint8Array>),
172
+ pumpStream(child.stderr as ReadableStream<Uint8Array>),
173
+ ]);
174
+ } finally {
175
+ await writer.close();
176
+ }
177
+
178
+ // Non-zero exit codes or signal-killed processes are considered cancelled if killed via signal
179
+ const exitCode = await child.exited;
180
+
181
+ const cancelled = exitCode === null || (exitCode !== 0 && (options?.signal?.aborted ?? false));
182
+
183
+ if (signal.timedOut()) {
184
+ const secs = Math.round(options!.timeout! / 1000);
185
+ return {
186
+ exitCode: undefined,
187
+ cancelled: true,
188
+ ...sink.dump(`Command timed out after ${secs} seconds`),
189
+ };
190
+ }
191
+
192
+ return {
193
+ exitCode: cancelled ? undefined : exitCode,
194
+ cancelled,
195
+ ...sink.dump(),
196
+ };
236
197
  }
package/src/core/index.ts CHANGED
@@ -49,3 +49,5 @@ export {
49
49
  type MCPToolsLoadResult,
50
50
  type MCPTransport,
51
51
  } from "./mcp/index";
52
+
53
+ export * as utils from "./utils";
@@ -13,6 +13,7 @@ import {
13
13
  import { join, resolve } from "node:path";
14
14
  import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
15
15
  import type { ImageContent, Message, TextContent } from "@oh-my-pi/pi-ai";
16
+ import { nanoid } from "nanoid";
16
17
  import { getAgentDir as getDefaultAgentDir } from "../config";
17
18
  import {
18
19
  type BashExecutionMessage,
@@ -196,11 +197,10 @@ export type ReadonlySessionManager = Pick<
196
197
  /** Generate a unique short ID (8 hex chars, collision-checked) */
197
198
  function generateId(byId: { has(id: string): boolean }): string {
198
199
  for (let i = 0; i < 100; i++) {
199
- const id = crypto.randomUUID().slice(0, 8);
200
+ const id = nanoid(8);
200
201
  if (!byId.has(id)) return id;
201
202
  }
202
- // Fallback to full UUID if somehow we have collisions
203
- return crypto.randomUUID();
203
+ return nanoid(); // fallback to full nanoid
204
204
  }
205
205
 
206
206
  /** Migrate v1 → v2: add id/parentId tree structure. Mutates in place. */
@@ -451,42 +451,108 @@ export function loadEntriesFromFile(filePath: string): FileEntry[] {
451
451
  return entries;
452
452
  }
453
453
 
454
- function isValidSessionFile(filePath: string): boolean {
455
- try {
456
- const fd = openSync(filePath, "r");
457
- const buffer = Buffer.alloc(512);
458
- const bytesRead = readSync(fd, buffer, 0, 512, 0);
459
- closeSync(fd);
460
- const firstLine = buffer.toString("utf8", 0, bytesRead).split("\n")[0];
461
- if (!firstLine) return false;
462
- const header = JSON.parse(firstLine);
463
- return header.type === "session" && typeof header.id === "string";
464
- } catch {
465
- return false;
454
+ /**
455
+ * Lightweight metadata for a session file, used in session picker UI.
456
+ * Uses lazy getters to defer string formatting until actually displayed.
457
+ */
458
+ class RecentSessionInfo {
459
+ readonly path: string;
460
+ readonly mtime: number;
461
+
462
+ #fullName: string | undefined;
463
+ #name: string | undefined;
464
+ #timeAgo: string | undefined;
465
+
466
+ constructor(path: string, mtime: number, header: Record<string, unknown>) {
467
+ this.path = path;
468
+ this.mtime = mtime;
469
+
470
+ // Extract title from session header, falling back to id if title is missing
471
+ const trystr = (v: unknown) => (typeof v === "string" ? v : undefined);
472
+ this.#fullName = trystr(header.title) ?? trystr(header.id);
473
+ }
474
+
475
+ /** Full session name from header, or filename without extension as fallback */
476
+ get fullName(): string {
477
+ if (this.#fullName) return this.#fullName;
478
+ this.#fullName = this.path.split("/").pop()?.replace(".jsonl", "") ?? "Unknown";
479
+ return this.#fullName;
480
+ }
481
+
482
+ /** Truncated name for display (max 40 chars) */
483
+ get name(): string {
484
+ if (this.#name) return this.#name;
485
+ const fullName = this.fullName;
486
+ this.#name = fullName.length <= 40 ? fullName : `${fullName.slice(0, 37)}...`;
487
+ return this.#name;
488
+ }
489
+
490
+ /** Human-readable relative time (e.g., "2 hours ago") */
491
+ get timeAgo(): string {
492
+ if (this.#timeAgo) return this.#timeAgo;
493
+ this.#timeAgo = formatTimeAgo(new Date(this.mtime));
494
+ return this.#timeAgo;
466
495
  }
467
496
  }
468
497
 
469
- /** Exported for testing */
470
- export function findMostRecentSession(sessionDir: string): string | null {
498
+ /**
499
+ * Reads all session files from the directory and returns them sorted by mtime (newest first).
500
+ * Uses low-level file I/O to efficiently read only the first 512 bytes of each file
501
+ * to extract the JSON header without loading entire session logs into memory.
502
+ */
503
+ function getSortedSessions(sessionDir: string): RecentSessionInfo[] {
471
504
  try {
472
- const files = readdirSync(sessionDir)
473
- .filter((f) => f.endsWith(".jsonl"))
474
- .map((f) => join(sessionDir, f))
475
- .filter(isValidSessionFile)
476
- .map((path) => ({ path, mtime: statSync(path).mtime }))
477
- .sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
478
-
479
- return files[0]?.path || null;
505
+ // Reusable buffer for reading file headers
506
+ const buf = Buffer.allocUnsafe(512);
507
+
508
+ /**
509
+ * Reads the first line (JSON header) from an open file descriptor.
510
+ * Returns null if the file is empty or doesn't start with valid JSON.
511
+ */
512
+ const readHeader = (fd: number) => {
513
+ const bytesRead = readSync(fd, buf, 0, 512, 0);
514
+ if (bytesRead === 0) return null;
515
+ const sub = buf.subarray(0, bytesRead);
516
+ // Quick check: first char must be '{' for valid JSON object
517
+ if (sub.at(0) !== "{".charCodeAt(0)) return null;
518
+ // Find end of first JSON line
519
+ const eol = sub.indexOf("}\n");
520
+ if (eol <= 0) return null;
521
+ const header = JSON.parse(sub.toString("utf8", 0, eol + 1));
522
+ // Validate session header
523
+ if (header.type !== "session" || typeof header.id !== "string") return null;
524
+ return header;
525
+ };
526
+
527
+ return readdirSync(sessionDir)
528
+ .map((f) => {
529
+ try {
530
+ if (!f.endsWith(".jsonl")) return null;
531
+ const path = join(sessionDir, f);
532
+ const fd = openSync(path, "r");
533
+ try {
534
+ const header = readHeader(fd);
535
+ if (!header) return null;
536
+ const mtime = statSync(path).mtimeMs;
537
+ return new RecentSessionInfo(path, mtime, header);
538
+ } finally {
539
+ closeSync(fd);
540
+ }
541
+ } catch {
542
+ return null;
543
+ }
544
+ })
545
+ .filter((x) => x !== null)
546
+ .sort((a, b) => b.mtime - a.mtime); // Sort newest first
480
547
  } catch {
481
- return null;
548
+ return [];
482
549
  }
483
550
  }
484
551
 
485
- /** Recent session info for display */
486
- export interface RecentSessionInfo {
487
- name: string;
488
- path: string;
489
- timeAgo: string;
552
+ /** Exported for testing */
553
+ export function findMostRecentSession(sessionDir: string): string | null {
554
+ const sessions = getSortedSessions(sessionDir);
555
+ return sessions[0]?.path || null;
490
556
  }
491
557
 
492
558
  /** Format a time difference as a human-readable string */
@@ -506,41 +572,7 @@ function formatTimeAgo(date: Date): string {
506
572
 
507
573
  /** Get recent sessions for display in welcome screen */
508
574
  export function getRecentSessions(sessionDir: string, limit = 3): RecentSessionInfo[] {
509
- try {
510
- const files = readdirSync(sessionDir)
511
- .filter((f) => f.endsWith(".jsonl"))
512
- .map((f) => join(sessionDir, f))
513
- .filter(isValidSessionFile)
514
- .map((path) => {
515
- const stat = statSync(path);
516
- // Try to get session title or id from first line
517
- let name = path.split("/").pop()?.replace(".jsonl", "") ?? "Unknown";
518
- try {
519
- const content = readFileSync(path, "utf-8");
520
- const firstLine = content.split("\n")[0];
521
- if (firstLine) {
522
- const header = JSON.parse(firstLine) as SessionHeader;
523
- if (header.type === "session") {
524
- // Prefer title over id
525
- name = header.title ?? header.id ?? name;
526
- }
527
- }
528
- } catch {
529
- // Use filename as fallback
530
- }
531
- return { path, name, mtime: stat.mtime };
532
- })
533
- .sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
534
- .slice(0, limit);
535
-
536
- return files.map((f) => ({
537
- name: f.name.length > 40 ? `${f.name.slice(0, 37)}...` : f.name,
538
- path: f.path,
539
- timeAgo: formatTimeAgo(f.mtime),
540
- }));
541
- } catch {
542
- return [];
543
- }
575
+ return getSortedSessions(sessionDir).slice(0, limit);
544
576
  }
545
577
 
546
578
  /**
@@ -588,7 +620,7 @@ export class SessionManager {
588
620
  if (existsSync(this.sessionFile)) {
589
621
  this.fileEntries = loadEntriesFromFile(this.sessionFile);
590
622
  const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
591
- this.sessionId = header?.id ?? crypto.randomUUID();
623
+ this.sessionId = header?.id ?? nanoid();
592
624
  this.sessionTitle = header?.title;
593
625
 
594
626
  if (migrateToCurrentVersion(this.fileEntries)) {
@@ -603,7 +635,7 @@ export class SessionManager {
603
635
  }
604
636
 
605
637
  newSession(options?: NewSessionOptions): string | undefined {
606
- this.sessionId = crypto.randomUUID();
638
+ this.sessionId = nanoid();
607
639
  const timestamp = new Date().toISOString();
608
640
  const header: SessionHeader = {
609
641
  type: "session",
@@ -676,16 +708,23 @@ export class SessionManager {
676
708
 
677
709
  setSessionTitle(title: string): void {
678
710
  this.sessionTitle = title;
679
- // Update the session file header with the title
711
+
712
+ // Update the in-memory header (so first flush includes title)
713
+ const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
714
+ if (header) {
715
+ header.title = title;
716
+ }
717
+
718
+ // Update the session file header with the title (if already flushed)
680
719
  if (this.persist && this.sessionFile && existsSync(this.sessionFile)) {
681
720
  try {
682
721
  const content = readFileSync(this.sessionFile, "utf-8");
683
722
  const lines = content.split("\n");
684
723
  if (lines.length > 0) {
685
- const header = JSON.parse(lines[0]) as SessionHeader;
686
- if (header.type === "session") {
687
- header.title = title;
688
- lines[0] = JSON.stringify(header);
724
+ const fileHeader = JSON.parse(lines[0]) as SessionHeader;
725
+ if (fileHeader.type === "session") {
726
+ fileHeader.title = title;
727
+ lines[0] = JSON.stringify(fileHeader);
689
728
  writeFileSync(this.sessionFile, lines.join("\n"));
690
729
  }
691
730
  }
@@ -1081,7 +1120,7 @@ export class SessionManager {
1081
1120
  // Filter out LabelEntry from path - we'll recreate them from the resolved map
1082
1121
  const pathWithoutLabels = path.filter((e) => e.type !== "label");
1083
1122
 
1084
- const newSessionId = crypto.randomUUID();
1123
+ const newSessionId = nanoid();
1085
1124
  const timestamp = new Date().toISOString();
1086
1125
  const fileTimestamp = timestamp.replace(/[:.]/g, "-");
1087
1126
  const newSessionFile = join(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);