@co0ontty/wand 2.4.0 → 2.4.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,6 +1,6 @@
1
1
  {
2
- "commit": "64b14513c83cef484b8635b6142a63e3cd19af6c",
3
- "builtAt": "2026-07-04T09:00:31.976Z",
4
- "version": "2.4.0",
2
+ "commit": "f3d51073dca38b18ea1104511c7fdebdd125b086",
3
+ "builtAt": "2026-07-04T10:40:24.303Z",
4
+ "version": "2.4.1",
5
5
  "channel": "stable"
6
6
  }
@@ -30,8 +30,10 @@ export interface CodexHistorySession {
30
30
  claudeSessionId: string;
31
31
  cwd: string;
32
32
  firstUserMessage: string;
33
+ firstUserAt: string;
33
34
  timestamp: string;
34
35
  mtimeMs: number;
36
+ hasUser: boolean;
35
37
  hasConversation: boolean;
36
38
  managedByWand: boolean;
37
39
  provider: "codex";
@@ -331,6 +331,7 @@ function readCodexSessionSummary(filePath) {
331
331
  let cwd = "";
332
332
  let timestamp = "";
333
333
  let firstUserMessage = "";
334
+ let firstUserAt = "";
334
335
  let hasUser = false;
335
336
  let hasAssistant = false;
336
337
  for (const line of lines) {
@@ -363,6 +364,9 @@ function readCodexSessionSummary(filePath) {
363
364
  : "";
364
365
  if (text.trim()) {
365
366
  hasUser = true;
367
+ if (!firstUserAt && parsed.timestamp) {
368
+ firstUserAt = parsed.timestamp;
369
+ }
366
370
  if (!firstUserMessage && !isCodexSystemInjectedText(text)) {
367
371
  firstUserMessage = text.trim().slice(0, 120);
368
372
  }
@@ -378,8 +382,10 @@ function readCodexSessionSummary(filePath) {
378
382
  claudeSessionId: id,
379
383
  cwd,
380
384
  firstUserMessage,
385
+ firstUserAt,
381
386
  timestamp: timestamp || new Date(stats.mtimeMs).toISOString(),
382
387
  mtimeMs: stats.mtimeMs,
388
+ hasUser,
383
389
  hasConversation: hasUser && hasAssistant,
384
390
  managedByWand: false,
385
391
  provider: "codex",
@@ -433,10 +439,19 @@ function isSameResolvedPath(left, right) {
433
439
  return false;
434
440
  return path.resolve(left) === path.resolve(right);
435
441
  }
442
+ function isUsableCodexHistorySession(session) {
443
+ return session.hasUser || session.hasConversation;
444
+ }
445
+ function parseTimeMs(value) {
446
+ if (!value)
447
+ return null;
448
+ const parsed = Date.parse(value);
449
+ return Number.isFinite(parsed) ? parsed : null;
450
+ }
436
451
  function selectCodexSessionForRecord(record) {
437
452
  const knownMtimes = record.knownCodexSessionMtimes ?? new Map();
438
453
  const candidates = listAllCodexHistorySessions()
439
- .filter((session) => session.hasConversation)
454
+ .filter(isUsableCodexHistorySession)
440
455
  .filter((session) => isSameResolvedPath(session.cwd, record.cwd))
441
456
  .filter((session) => {
442
457
  const previousMtime = knownMtimes.get(session.claudeSessionId);
@@ -459,6 +474,36 @@ function selectCodexSessionForRecord(record) {
459
474
  function getLatestCodexSessionId(record) {
460
475
  return selectCodexSessionForRecord(record)?.claudeSessionId ?? null;
461
476
  }
477
+ function selectCodexSessionForTimeWindow(record) {
478
+ const startedAtMs = parseTimeMs(record.startedAt);
479
+ if (startedAtMs === null)
480
+ return null;
481
+ const endedAtMs = parseTimeMs(record.endedAt) ?? Date.now();
482
+ const windowStart = startedAtMs - START_TIME_SKEW_MS;
483
+ const windowEnd = endedAtMs + START_TIME_SKEW_MS;
484
+ const candidates = listAllCodexHistorySessions()
485
+ .filter(isUsableCodexHistorySession)
486
+ .filter((session) => isSameResolvedPath(session.cwd, record.cwd))
487
+ .filter((session) => {
488
+ const firstUserAtMs = parseTimeMs(session.firstUserAt);
489
+ if (firstUserAtMs !== null) {
490
+ return firstUserAtMs >= windowStart && firstUserAtMs <= windowEnd;
491
+ }
492
+ return session.mtimeMs >= windowStart && session.mtimeMs <= endedAtMs + DISCOVERY_RECENT_WINDOW_MS;
493
+ })
494
+ .sort((a, b) => {
495
+ const aTime = parseTimeMs(a.firstUserAt) ?? a.mtimeMs;
496
+ const bTime = parseTimeMs(b.firstUserAt) ?? b.mtimeMs;
497
+ return Math.abs(aTime - startedAtMs) - Math.abs(bTime - startedAtMs);
498
+ });
499
+ return candidates.length === 1 ? candidates[0] : null;
500
+ }
501
+ function recoverCodexSessionIdFromHistory(snapshot) {
502
+ if (snapshot.provider !== "codex" || snapshot.claudeSessionId) {
503
+ return null;
504
+ }
505
+ return getCodexResumeCommandSessionId(snapshot.command) ?? selectCodexSessionForTimeWindow(snapshot)?.claudeSessionId ?? null;
506
+ }
462
507
  /** Delete every rollout file belonging to the given codex thread ids. */
463
508
  function deleteCodexRolloutFiles(threadIds) {
464
509
  if (threadIds.size === 0)
@@ -597,6 +642,15 @@ export class ProcessManager extends EventEmitter {
597
642
  : isCodexCmd
598
643
  ? getCodexResumeCommandSessionId(snapshot.command)
599
644
  : null;
645
+ const orphanEndedAt = snapshot.status === "running" ? new Date().toISOString() : null;
646
+ const sessionIdFromHistory = isCodexCmd
647
+ ? recoverCodexSessionIdFromHistory({
648
+ ...snapshot,
649
+ provider: "codex",
650
+ endedAt: snapshot.endedAt ?? orphanEndedAt,
651
+ })
652
+ : null;
653
+ const restoredSessionId = resumeCommandSessionId ?? snapshot.claudeSessionId ?? sessionIdFromHistory;
600
654
  // Sessions restored from storage have ptyProcess: null — the old server's PTY
601
655
  // belongs to a dead process. Mark running sessions as exited so the UI
602
656
  // reflects reality and users can start fresh sessions.
@@ -605,10 +659,14 @@ export class ProcessManager extends EventEmitter {
605
659
  const updated = {
606
660
  ...snapshot,
607
661
  status: "exited",
608
- endedAt: new Date().toISOString(),
662
+ endedAt: orphanEndedAt,
663
+ claudeSessionId: restoredSessionId ?? null,
609
664
  messages: recoveredMessages.length > 0 ? recoveredMessages : snapshot.messages,
610
665
  };
611
666
  this.storage.saveSession(updated);
667
+ if (isCodexCmd && restoredSessionId && restoredSessionId !== snapshot.claudeSessionId) {
668
+ process.stderr.write(`[wand] Recovered Codex thread ID for orphan PTY ${snapshot.id}: ${restoredSessionId}\n`);
669
+ }
612
670
  this.sessions.set(snapshot.id, {
613
671
  ...updated,
614
672
  provider,
@@ -641,7 +699,7 @@ export class ProcessManager extends EventEmitter {
641
699
  knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(updated.cwd) : undefined,
642
700
  knownCodexSessionMtimes: isCodexCmd ? listCodexSessionMtimes() : undefined,
643
701
  codexSessionDiscoveryTimer: null,
644
- claudeSessionId: resumeCommandSessionId ?? updated.claudeSessionId,
702
+ claudeSessionId: restoredSessionId ?? updated.claudeSessionId,
645
703
  approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
646
704
  ptyCols: snapshot.ptyCols ?? 120,
647
705
  ptyRows: snapshot.ptyRows ?? 36,
@@ -649,8 +707,17 @@ export class ProcessManager extends EventEmitter {
649
707
  this.orphanRecoveredCount += 1;
650
708
  }
651
709
  else {
710
+ const updated = restoredSessionId && restoredSessionId !== snapshot.claudeSessionId
711
+ ? { ...snapshot, claudeSessionId: restoredSessionId }
712
+ : snapshot;
713
+ if (updated !== snapshot) {
714
+ this.storage.saveSessionMetadata(updated);
715
+ if (isCodexCmd) {
716
+ process.stderr.write(`[wand] Recovered Codex thread ID for saved PTY ${snapshot.id}: ${restoredSessionId}\n`);
717
+ }
718
+ }
652
719
  this.sessions.set(snapshot.id, {
653
- ...snapshot,
720
+ ...updated,
654
721
  provider,
655
722
  processId: null,
656
723
  ptyProcess: null,
@@ -676,10 +743,10 @@ export class ProcessManager extends EventEmitter {
676
743
  lastEmittedTask: null,
677
744
  knownClaudeTaskIds: undefined,
678
745
  claudeTaskDiscoveryTimer: null,
679
- knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(snapshot.cwd) : undefined,
746
+ knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(updated.cwd) : undefined,
680
747
  knownCodexSessionMtimes: isCodexCmd ? listCodexSessionMtimes() : undefined,
681
748
  codexSessionDiscoveryTimer: null,
682
- claudeSessionId: resumeCommandSessionId ?? snapshot.claudeSessionId,
749
+ claudeSessionId: restoredSessionId ?? updated.claudeSessionId,
683
750
  approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
684
751
  ptyCols: snapshot.ptyCols ?? 120,
685
752
  ptyRows: snapshot.ptyRows ?? 36,
@@ -928,7 +995,7 @@ export class ProcessManager extends EventEmitter {
928
995
  }
929
996
  current.pendingEscalation = null;
930
997
  current.ptyPermissionBlocked = false;
931
- this.captureCodexSessionId(current);
998
+ this.captureCodexSessionId(current, { allowTimeWindowFallback: true });
932
999
  current.status = current.stopRequested ? "stopped" : exitCode === 0 ? "exited" : "failed";
933
1000
  current.exitCode = current.stopRequested ? null : exitCode;
934
1001
  current.endedAt = new Date().toISOString();
@@ -1175,22 +1242,30 @@ export class ProcessManager extends EventEmitter {
1175
1242
  }
1176
1243
  return deleted;
1177
1244
  }
1178
- captureCodexSessionId(record) {
1179
- if (record.provider !== "codex" || record.claudeSessionId || !record.knownCodexSessionMtimes) {
1245
+ captureCodexSessionId(record, options) {
1246
+ if (record.provider !== "codex" || record.claudeSessionId) {
1180
1247
  return false;
1181
1248
  }
1182
- const discoveredThreadId = getLatestCodexSessionId({
1183
- cwd: record.cwd,
1184
- startedAt: record.startedAt,
1185
- knownCodexSessionMtimes: record.knownCodexSessionMtimes,
1186
- });
1187
- if (!discoveredThreadId) {
1249
+ const discoveredThreadId = record.knownCodexSessionMtimes
1250
+ ? getLatestCodexSessionId({
1251
+ cwd: record.cwd,
1252
+ startedAt: record.startedAt,
1253
+ knownCodexSessionMtimes: record.knownCodexSessionMtimes,
1254
+ })
1255
+ : null;
1256
+ const fallbackThreadId = discoveredThreadId
1257
+ ? null
1258
+ : options?.allowTimeWindowFallback
1259
+ ? selectCodexSessionForTimeWindow(record)?.claudeSessionId ?? null
1260
+ : null;
1261
+ const threadId = discoveredThreadId ?? fallbackThreadId;
1262
+ if (!threadId) {
1188
1263
  return false;
1189
1264
  }
1190
- record.claudeSessionId = discoveredThreadId;
1191
- record.knownCodexSessionMtimes.set(discoveredThreadId, Date.now());
1265
+ record.claudeSessionId = threadId;
1266
+ record.knownCodexSessionMtimes?.set(threadId, Date.now());
1192
1267
  this.codexHistoryCache = null;
1193
- process.stderr.write(`[wand] Captured Codex thread ID: ${discoveredThreadId}\n`);
1268
+ process.stderr.write(`[wand] Captured Codex thread ID: ${threadId}\n`);
1194
1269
  return true;
1195
1270
  }
1196
1271
  get(id) {
@@ -1388,7 +1463,7 @@ export class ProcessManager extends EventEmitter {
1388
1463
  record.ptyBridge = null;
1389
1464
  }
1390
1465
  // Update lifecycle
1391
- this.captureCodexSessionId(record);
1466
+ this.captureCodexSessionId(record, { allowTimeWindowFallback: true });
1392
1467
  this.persist(record);
1393
1468
  return this.snapshot(record);
1394
1469
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@co0ontty/wand",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "A web terminal for local CLI tools like Claude.",
5
5
  "type": "module",
6
6
  "bin": {