Skip to content

Commit f997b96

Browse files
Myoontyeeclaude
andcommitted
fix: replace fs.watch with 30s polling for Codex sessions (v0.8.8)
fs.watch { recursive:true } on Windows fails to detect new files when Codex creates a new date subdirectory (YYYY/MM/DD/) mid-session. Polling every 30s via scanCodexSessions() reliably catches all new/modified .jsonl files regardless of directory structure changes. Tracks exported mtime per file to avoid re-exporting unchanged sessions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 1a4a975 commit f997b96

File tree

2 files changed

+47
-40
lines changed

2 files changed

+47
-40
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "claude-code-exporter",
33
"displayName": "Claude Code Exporter — Claude Code, Codex & Cursor Conversation History",
44
"description": "Export AI coding agent conversations to Markdown. Supports Claude Code (→ .claude-code-history/), OpenAI Codex (→ .codex-history/), and Cursor Composer (→ .cursor-history/). Features: auto-watch, session inject/import for claude --resume, and repair of thinking-block signature errors when switching API providers.",
5-
"version": "0.8.7",
5+
"version": "0.8.8",
66
"publisher": "myoontyee",
77
"engines": {
88
"vscode": "^1.85.0"

src/codexWatcher.ts

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
/**
2-
* codexWatcher.ts — Watch ~/.codex/sessions/ for new Codex CLI sessions
2+
* codexWatcher.ts — Watch ~/.codex/sessions/ for new Codex CLI/APP sessions
33
* and auto-export to .codex-history/
4+
*
5+
* Uses a polling approach (scan every 30s) instead of fs.watch because
6+
* fs.watch { recursive:true } on Windows misses files created inside newly
7+
* added subdirectories (the YYYY/MM/DD/ structure Codex creates daily).
48
*/
59

610
import * as fs from 'fs';
711
import * as path from 'path';
812
import * as os from 'os';
913
import * as vscode from 'vscode';
1014
import { CodexExporter } from './codexExporter';
15+
import { scanCodexSessions } from './codexParser';
16+
17+
const POLL_INTERVAL_MS = 30_000;
1118

1219
export class CodexWatcher implements vscode.Disposable {
13-
private watcher?: fs.FSWatcher;
14-
private debounceTimers = new Map<string, NodeJS.Timeout>();
20+
private pollTimer?: NodeJS.Timeout;
1521
private disposed = false;
16-
private codexSessionsDir: string;
22+
private readonly codexSessionsDir: string;
23+
// Track files we've already exported: path → last mtime we exported at
24+
private readonly exported = new Map<string, number>();
1725

1826
constructor(
1927
private readonly workspaceRoot: string,
@@ -28,56 +36,55 @@ export class CodexWatcher implements vscode.Disposable {
2836
return;
2937
}
3038

31-
// Watch recursively (YYYY/MM/DD/ structure)
32-
this.watcher = fs.watch(
33-
this.codexSessionsDir,
34-
{ recursive: true },
35-
(_event, filename) => {
36-
if (!filename || !filename.endsWith('.jsonl')) return;
37-
38-
const fullPath = path.join(this.codexSessionsDir, filename);
39-
const existing = this.debounceTimers.get(fullPath);
40-
if (existing) clearTimeout(existing);
41-
42-
const timer = setTimeout(() => {
43-
this.debounceTimers.delete(fullPath);
44-
this.handleChange(fullPath);
45-
}, 500);
46-
47-
this.debounceTimers.set(fullPath, timer);
48-
}
49-
);
50-
51-
this.watcher.on('error', (err) => {
52-
console.error('[codex-watcher] Watcher error:', err);
53-
});
39+
// Run once immediately, then on interval
40+
this.poll();
41+
this.pollTimer = setInterval(() => this.poll(), POLL_INTERVAL_MS);
5442

5543
console.log(
56-
`[codex-watcher] Watching ${this.codexSessionsDir}${this.workspaceRoot}/.codex-history/`
44+
`[codex-watcher] Polling ${this.codexSessionsDir} every ${POLL_INTERVAL_MS / 1000}s${this.workspaceRoot}/.codex-history/`
5745
);
5846
}
5947

60-
private handleChange(filePath: string): void {
61-
if (this.disposed || !fs.existsSync(filePath)) return;
48+
private poll(): void {
49+
if (this.disposed) return;
6250

6351
const cfg = vscode.workspace.getConfiguration('claudeCodeExporter');
6452
if (!cfg.get<boolean>('autoExport')) return;
6553

54+
let files: string[];
6655
try {
67-
const outPath = this.exporter.exportSessionToWorkspace(
68-
filePath,
69-
this.workspaceRoot
70-
);
71-
console.log(`[codex-watcher] Auto-exported → ${outPath}`);
56+
files = scanCodexSessions(this.codexSessionsDir);
7257
} catch (err) {
73-
console.error(`[codex-watcher] Export failed: ${filePath}`, err);
58+
console.error('[codex-watcher] Scan failed:', err);
59+
return;
60+
}
61+
62+
for (const filePath of files) {
63+
let mtime: number;
64+
try {
65+
mtime = fs.statSync(filePath).mtimeMs;
66+
} catch {
67+
continue;
68+
}
69+
70+
const lastExported = this.exported.get(filePath) ?? 0;
71+
if (mtime <= lastExported) continue; // not changed since last export
72+
73+
try {
74+
const outPath = this.exporter.exportSessionToWorkspace(filePath, this.workspaceRoot);
75+
this.exported.set(filePath, mtime);
76+
console.log(`[codex-watcher] Auto-exported → ${outPath}`);
77+
} catch (err) {
78+
console.error(`[codex-watcher] Export failed: ${filePath}`, err);
79+
}
7480
}
7581
}
7682

7783
dispose(): void {
7884
this.disposed = true;
79-
this.watcher?.close();
80-
for (const t of this.debounceTimers.values()) clearTimeout(t);
81-
this.debounceTimers.clear();
85+
if (this.pollTimer) {
86+
clearInterval(this.pollTimer);
87+
this.pollTimer = undefined;
88+
}
8289
}
8390
}

0 commit comments

Comments
 (0)