@oh-my-pi/pi-utils 18.4.10 → 18.4.12

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,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.4.12] - 2026-10-02
6
+
7
+ ### Fixed
8
+
9
+ - Fixed multi-second temp directory removal stalls on Windows by forcing a major GC before the first deletion retry ([#13044](https://github.com/can1357/oh-my-pi/pull/13044) by [@jchanghong023](https://github.com/jchanghong023)).
10
+
11
+ ## [18.4.11] - 2026-10-02
12
+
13
+ ### Added
14
+
15
+ - Added XDG-aware utility paths for skill descriptions and prediction state, with automatic adoption of legacy data when XDG locations are first resolved.
16
+
17
+ ### Fixed
18
+
19
+ - Fixed machine-global daemon runtime paths so brokers such as text prediction use the shared XDG state location across profiles and custom agent directories.
20
+
5
21
  ## [18.4.10] - 2026-10-02
6
22
 
7
23
  ### Added
@@ -277,6 +277,10 @@ export declare function getTinyModelsCacheDir(agentDir?: string): string;
277
277
  export declare function getDocumentConversionCacheDir(agentDir?: string): string;
278
278
  /** Get the composer speculative cache database (~/.omp/agent/cache/composer.db; XDG default: $XDG_CACHE_HOME/omp/cache/composer.db). */
279
279
  export declare function getComposerCacheDbPath(agentDir?: string): string;
280
+ /** Get the skill descriptions database (~/.omp/agent/skill-descriptions.db; XDG default: $XDG_DATA_HOME/omp/skill-descriptions.db). */
281
+ export declare function getSkillDescriptionsDbPath(agentDir?: string): string;
282
+ /** Get the text-predict engine state directory (~/.omp/agent/predict/<method>; XDG default: $XDG_DATA_HOME/omp/predict/<method>). Adopts legacy engine state on first XDG resolution. */
283
+ export declare function getPredictStateDir(agentDir: string | undefined, method: string): string;
280
284
  /** Get the sessions directory (~/.omp/agent/sessions). */
281
285
  export declare function getSessionsDir(agentDir?: string): string;
282
286
  /** Get the content-addressed blob store directory (~/.omp/agent/blobs). */
@@ -314,7 +318,7 @@ export declare function getTinyWorkerRuntimeDir(): string;
314
318
  export declare function getDaemonRuntimeRoot(): string;
315
319
  /** Get the daemon runtime directory for a project (~/.omp/run/daemons/<hash>; XDG default: $XDG_STATE_HOME/omp/run/daemons/<hash>). */
316
320
  export declare function getDaemonRuntimeDir(projectDir: string): string;
317
- /** Root directory containing every machine-global daemon service scope. */
321
+ /** Root directory containing every machine-global daemon service scope (~/.omp/run/daemons/global; XDG default: $XDG_STATE_HOME/omp/run/daemons/global). Shared across profiles. */
318
322
  export declare function getGlobalDaemonRuntimeRoot(): string;
319
323
  /** Get a profile-independent runtime directory for a machine-global daemon service. */
320
324
  export declare function getGlobalDaemonRuntimeDir(service: string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-utils",
3
- "version": "18.4.10",
3
+ "version": "18.4.12",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "cli",
@@ -57,7 +57,7 @@
57
57
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
58
58
  },
59
59
  "dependencies": {
60
- "@oh-my-pi/pi-natives": "18.4.10"
60
+ "@oh-my-pi/pi-natives": "18.4.12"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@types/bun": "^1.3.14"
package/src/dirs.ts CHANGED
@@ -334,6 +334,7 @@ class DirResolver {
334
334
  // With XDG on Linux, they point to $XDG_*_HOME/omp/.
335
335
  readonly #rootDirs: Record<XdgCategory, string>;
336
336
  readonly #agentDirs: Record<XdgCategory, string>;
337
+ readonly #baseRootDirs: Record<XdgCategory, string>;
337
338
 
338
339
  readonly #rootCache = new Map<string, string>();
339
340
  readonly #agentCache = new Map<string, string>();
@@ -362,7 +363,8 @@ class DirResolver {
362
363
  let xdgData: string | undefined;
363
364
  let xdgState: string | undefined;
364
365
  let xdgCache: string | undefined;
365
- if ((process.platform === "linux" || process.platform === "darwin") && isDefault) {
366
+ const xdgPlatform = process.platform === "linux" || process.platform === "darwin";
367
+ if (xdgPlatform && isDefault) {
366
368
  const resolveIf = (envVar: string) => {
367
369
  const value = process.env[envVar];
368
370
  if (!value) return undefined;
@@ -370,14 +372,10 @@ class DirResolver {
370
372
  const appRoot = path.join(value, APP_NAME);
371
373
  if (profile) {
372
374
  const profilePath = path.join(appRoot, "profiles", profile);
373
- if (fs.existsSync(profilePath)) {
374
- return profilePath;
375
- }
375
+ if (fs.existsSync(profilePath)) return profilePath;
376
376
  return undefined;
377
377
  }
378
- if (fs.existsSync(appRoot)) {
379
- return appRoot;
380
- }
378
+ return fs.existsSync(appRoot) ? appRoot : undefined;
381
379
  } catch {}
382
380
  return undefined;
383
381
  };
@@ -386,6 +384,22 @@ class DirResolver {
386
384
  xdgCache = resolveIf("XDG_CACHE_HOME");
387
385
  }
388
386
 
387
+ // XDG choice for machine-global paths (daemon scopes shared by every
388
+ // process): keyed only on the base app root, independent of both the
389
+ // profile and any agent-dir override, so every omp process on the machine
390
+ // agrees on one location. These hold process-scoped runtime state
391
+ // (sockets, tokens), so there is no migration to protect.
392
+ const resolveBase = (envVar: string) => {
393
+ if (!xdgPlatform) return undefined;
394
+ const value = process.env[envVar];
395
+ if (!value) return undefined;
396
+ try {
397
+ const appRoot = path.join(value, APP_NAME);
398
+ return fs.existsSync(appRoot) ? appRoot : undefined;
399
+ } catch {}
400
+ return undefined;
401
+ };
402
+
389
403
  this.#rootDirs = {
390
404
  data: xdgData ?? this.configRoot,
391
405
  state: xdgState ?? this.configRoot,
@@ -397,6 +411,17 @@ class DirResolver {
397
411
  state: xdgState ?? this.agentDir,
398
412
  cache: xdgCache ?? this.agentDir,
399
413
  };
414
+ const baseRoot = getBaseConfigRoot();
415
+ this.#baseRootDirs = {
416
+ data: resolveBase("XDG_DATA_HOME") ?? baseRoot,
417
+ state: resolveBase("XDG_STATE_HOME") ?? baseRoot,
418
+ cache: resolveBase("XDG_CACHE_HOME") ?? baseRoot,
419
+ };
420
+ }
421
+
422
+ /** Profile-independent config-root subdirectory, with optional XDG override. Shared across profiles. */
423
+ baseRootSubdir(subdir: string, xdg?: XdgCategory): string {
424
+ return path.join(xdg ? this.#baseRootDirs[xdg] : getBaseConfigRoot(), subdir);
400
425
  }
401
426
 
402
427
  /** Config-root subdirectory, with optional XDG override. */
@@ -915,6 +940,17 @@ export function getDocumentConversionCacheDir(agentDir?: string): string {
915
940
  export function getComposerCacheDbPath(agentDir?: string): string {
916
941
  return dirs.agentSubdir(agentDir, path.join("cache", "composer.db"), "cache");
917
942
  }
943
+ /** Get the skill descriptions database (~/.omp/agent/skill-descriptions.db; XDG default: $XDG_DATA_HOME/omp/skill-descriptions.db). */
944
+ export function getSkillDescriptionsDbPath(agentDir?: string): string {
945
+ return dirs.agentSubdir(agentDir, "skill-descriptions.db", "data");
946
+ }
947
+ /** Get the text-predict engine state directory (~/.omp/agent/predict/<method>; XDG default: $XDG_DATA_HOME/omp/predict/<method>). Adopts legacy engine state on first XDG resolution. */
948
+ export function getPredictStateDir(agentDir: string | undefined, method: string): string {
949
+ const subdir = path.join("predict", method);
950
+ const stateDir = dirs.agentSubdir(agentDir, subdir, "data");
951
+ adoptLegacyDir(path.join(agentDir ?? dirs.agentDir, subdir), stateDir);
952
+ return stateDir;
953
+ }
918
954
 
919
955
  /** Get the sessions directory (~/.omp/agent/sessions). */
920
956
  export function getSessionsDir(agentDir?: string): string {
@@ -1000,6 +1036,29 @@ function adoptLegacyFile(legacyPath: string, targetPath: string): void {
1000
1036
  }
1001
1037
  }
1002
1038
 
1039
+ /**
1040
+ * Best-effort one-time copy of a legacy directory to its redirected XDG
1041
+ * location, so learned state survives enabling XDG. The copy is staged next to
1042
+ * the target and renamed into place, so a reader never sees a partial tree and
1043
+ * a concurrent adopter cannot clobber a finished one. The legacy directory is
1044
+ * left in place for older omp versions sharing the profile.
1045
+ */
1046
+ function adoptLegacyDir(legacyPath: string, targetPath: string): void {
1047
+ if (targetPath === legacyPath) return;
1048
+ const staging = `${targetPath}.adopt-${process.pid}`;
1049
+ try {
1050
+ if (fs.existsSync(targetPath) || !fs.statSync(legacyPath, { throwIfNoEntry: false })?.isDirectory()) return;
1051
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
1052
+ fs.rmSync(staging, { recursive: true, force: true });
1053
+ fs.cpSync(legacyPath, staging, { recursive: true });
1054
+ fs.renameSync(staging, targetPath);
1055
+ } catch {
1056
+ // Opportunistic: a lost race or unwritable XDG dir falls back to fresh
1057
+ // state at the new path — the pre-adoption behavior.
1058
+ fs.rmSync(staging, { recursive: true, force: true });
1059
+ }
1060
+ }
1061
+
1003
1062
  /** Get the secret placeholder key path (~/.omp/agent/secret-placeholder.key; XDG default: $XDG_STATE_HOME/omp/secret-placeholder.key). Adopts a legacy key on first XDG resolution. */
1004
1063
  export function getSecretPlaceholderKeyPath(): string {
1005
1064
  const keyPath = dirs.agentSubdir(undefined, "secret-placeholder.key", "state");
@@ -1023,9 +1082,9 @@ export function getDaemonRuntimeDir(projectDir: string): string {
1023
1082
  return path.join(getDaemonRuntimeRoot(), key);
1024
1083
  }
1025
1084
 
1026
- /** Root directory containing every machine-global daemon service scope. */
1085
+ /** Root directory containing every machine-global daemon service scope (~/.omp/run/daemons/global; XDG default: $XDG_STATE_HOME/omp/run/daemons/global). Shared across profiles. */
1027
1086
  export function getGlobalDaemonRuntimeRoot(): string {
1028
- return path.join(getBaseConfigRoot(), "run", "daemons", "global");
1087
+ return dirs.baseRootSubdir(path.join("run", "daemons", "global"), "state");
1029
1088
  }
1030
1089
 
1031
1090
  /** Get a profile-independent runtime directory for a machine-global daemon service. */
package/src/temp.ts CHANGED
@@ -94,6 +94,11 @@ export async function removeWithRetries(target: string): Promise<void> {
94
94
  return;
95
95
  } catch (err) {
96
96
  if (!shouldRetryRemove(err, attempt)) throw err;
97
+ // bun on Windows finalizes SQLite's db/-wal/-shm file and directory
98
+ // handles on GC, so a closed database can still block deletion for
99
+ // seconds. One forced major collection on the first retry releases
100
+ // them immediately instead of burning the retry window.
101
+ if (attempt === 0) Bun.gc(true);
97
102
  await Bun.sleep(kRemoveRetryDelayMs);
98
103
  }
99
104
  }
@@ -106,6 +111,7 @@ export function removeSyncWithRetries(target: string): void {
106
111
  return;
107
112
  } catch (err) {
108
113
  if (!shouldRetryRemove(err, attempt)) throw err;
114
+ if (attempt === 0) Bun.gc(true);
109
115
  sleepSync(kRemoveRetryDelayMs);
110
116
  }
111
117
  }