@linxiraos/pi-utils 1.4.6 → 1.4.7

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
@@ -1,8 +1,18 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.4.7] - 2026-10-30
4
+
5
+ - Synced with upstream OMP v18.14.0; no package-specific user-visible changes.
6
+
3
7
  ## [1.4.6] - 2026-10-28
4
8
 
5
- - Released as part of Zeta 1.4.6; no package-specific user-visible changes.
9
+ ### Added
10
+
11
+ - Added `logger.withContext(fields, fn)` to attach contextual fields to all records produced during a function and its asynchronous work, plus `logger.withoutContext(fn)` for work that should not inherit caller context.
12
+
13
+ ### Fixed
14
+
15
+ - Fixed `untilAborted` so promises passed to an already-aborted signal are still observed, preventing later rejections from being reported as unhandled.
6
16
 
7
17
  ## [1.4.5] - 2026-10-28
8
18
 
package/README.md CHANGED
@@ -29,7 +29,7 @@ Import from the root barrel or per-module subpaths (`@linxiraos/pi-utils/<module
29
29
  bun add @linxiraos/pi-utils
30
30
  ```
31
31
 
32
- Ships TypeScript source directly (no build step); requires Bun ≥ 1.3.14.
32
+ Ships TypeScript source directly (no build step); requires Bun ≥ 1.4.4.
33
33
 
34
34
  ## References
35
35
 
@@ -16,7 +16,8 @@ export declare class AbortError extends Error {
16
16
  export declare function abortableSource<T>(stream: ReadableStream<T>, signal?: AbortSignal): AsyncGenerator<T>;
17
17
  /**
18
18
  * Runs a promise-returning function (`pr`). If the given AbortSignal is aborted before or during
19
- * execution, the promise is rejected with a standard error.
19
+ * execution, the promise is rejected with a standard error. A promise passed directly stays
20
+ * observed, so its later rejection is never unhandled.
20
21
  *
21
22
  * @param signal - Optional AbortSignal to cancel the operation
22
23
  * @param pr - Function returning a promise to run
@@ -10,7 +10,6 @@ export * from "./file-lock.js";
10
10
  export * from "./format.js";
11
11
  export * from "./frontmatter.js";
12
12
  export * from "./fs-error.js";
13
- export * from "./fs-open.js";
14
13
  export * from "./incoming-json.js";
15
14
  export * from "./json.js";
16
15
  export * from "./json-parse.js";
@@ -11,6 +11,20 @@ export interface LogEvent {
11
11
  export type LogSink = (event: LogEvent) => void;
12
12
  /** Register an out-of-band log sink and return a disposer. */
13
13
  export declare function registerLogSink(sink: LogSink): () => void;
14
+ /**
15
+ * Run `fn` with `context`'s fields on every record logged while it runs,
16
+ * including from the promises, timers and callbacks it starts. A nested call
17
+ * replaces the enclosing context, as a subagent's session replaces its
18
+ * parent's; a field passed to the log call itself wins. Fields are read as each
19
+ * record is written, so a getter stays current.
20
+ */
21
+ export declare function withContext<T>(context: Readonly<Record<string, unknown>>, fn: () => T): T;
22
+ /**
23
+ * Run `fn` outside any {@link withContext}: for process-wide work started on
24
+ * behalf of one caller, such as a shared server's read loop, whose later
25
+ * records belong to no single caller.
26
+ */
27
+ export declare function withoutContext<T>(fn: () => T): T;
14
28
  /**
15
29
  * Write buffered file records to disk now. The file transport batches records
16
30
  * (one write per second or per 64 KiB) and writes `warn`/`error` records
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * OMP installs on the same machine use 30141/30142/3847, and common developer
5
5
  * tools squat on 3000/8080/8787/3847. Zeta therefore owns a dedicated block:
6
- * three uncommon ports, at least two apart, so a side-by-side OMP install and
7
- * unrelated services cannot take the gateway or the stats dashboard away.
6
+ * uncommon ports, at least two apart, so a side-by-side OMP install and
7
+ * unrelated services cannot take the gateway, stats dashboard, or browser
8
+ * relay away.
8
9
  *
9
10
  * Every default in the repository derives from these constants. The standalone
10
11
  * Electron shell (`desktop/src/main.ts`, not a Bun workspace package) carries
@@ -13,3 +14,4 @@
13
14
  export declare const ZETA_WEB_PORT = 43141;
14
15
  export declare const ZETA_GATEWAY_PORT = 43143;
15
16
  export declare const ZETA_STATS_PORT = 43147;
17
+ export declare const ZETA_BROWSER_RELAY_PORT = 43153;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linxiraos/pi-utils",
3
- "version": "1.4.6",
3
+ "version": "1.4.7",
4
4
  "description": "Shared utilities for pi packages",
5
5
  "keywords": [
6
6
  "utilities",
@@ -54,12 +54,12 @@
54
54
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
55
55
  },
56
56
  "dependencies": {
57
- "@linxiraos/pi-natives": "1.4.6"
57
+ "@linxiraos/pi-natives": "1.4.7"
58
58
  },
59
59
  "devDependencies": {
60
- "@types/bun": "1.4.3"
60
+ "@types/bun": "1.4.4"
61
61
  },
62
62
  "engines": {
63
- "bun": ">=1.3.14"
63
+ "bun": ">=1.4.4"
64
64
  }
65
65
  }
package/src/abortable.ts CHANGED
@@ -60,7 +60,8 @@ export async function* abortableSource<T>(stream: ReadableStream<T>, signal?: Ab
60
60
 
61
61
  /**
62
62
  * Runs a promise-returning function (`pr`). If the given AbortSignal is aborted before or during
63
- * execution, the promise is rejected with a standard error.
63
+ * execution, the promise is rejected with a standard error. A promise passed directly stays
64
+ * observed, so its later rejection is never unhandled.
64
65
  *
65
66
  * @param signal - Optional AbortSignal to cancel the operation
66
67
  * @param pr - Function returning a promise to run
@@ -71,7 +72,10 @@ export function untilAborted<T>(
71
72
  pr: Promise<T> | (() => Promise<T>),
72
73
  ): Promise<T> {
73
74
  if (!signal) return typeof pr === "function" ? pr() : pr;
74
- if (signal.aborted) return Promise.reject(new AbortError(signal));
75
+ if (signal.aborted) {
76
+ if (typeof pr !== "function") void pr.catch(() => {});
77
+ return Promise.reject(new AbortError(signal));
78
+ }
75
79
 
76
80
  const { promise, resolve, reject } = Promise.withResolvers<T>();
77
81
  const onAbort = () => reject(new AbortError(signal));
package/src/index.ts CHANGED
@@ -10,7 +10,6 @@ export * from "./file-lock";
10
10
  export * from "./format";
11
11
  export * from "./frontmatter";
12
12
  export * from "./fs-error";
13
- export * from "./fs-open";
14
13
  export * from "./incoming-json";
15
14
  export * from "./json";
16
15
  export * from "./json-parse";
@@ -3,7 +3,6 @@ import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
5
  import { localDay } from "../dirs";
6
- import { openCloexecSync } from "../fs-open";
7
6
 
8
7
  /** Write batching for {@link RotatingFileSink}. */
9
8
  export interface RotatingFileBatchOptions {
@@ -87,7 +86,7 @@ export class RotatingFileSink {
87
86
 
88
87
  #openFd(filePath: string): void {
89
88
  this.#closeFd();
90
- this.#fd = openCloexecSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
89
+ this.#fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
91
90
  try {
92
91
  this.#onRotate?.(filePath);
93
92
  } catch {
package/src/logger.ts CHANGED
@@ -7,11 +7,13 @@
7
7
  * transport so a process supervisor (pm2, journald, k8s) captures the logs.
8
8
  *
9
9
  * Each entry includes `process.pid` so concurrent zeta instances stay
10
- * traceable. The file is created on the first record written. Records are
11
- * batched — one write per second or per 64 KiB — while `warn`/`error` records
12
- * are written at once together with everything buffered before them; exit,
13
- * fatal, and signal paths flush the rest. `ZETA_LOG_LEVEL` optionally limits
14
- * the levels written to the file (default: all).
10
+ * traceable, and the fields of the enclosing {@link withContext} (a session's
11
+ * `sessionId`) so sessions sharing one process stay apart. The file is created
12
+ * on the first record written. Records are batched — one write per second or
13
+ * per 64 KiB — while `warn`/`error` records are written at once together with
14
+ * everything buffered before them; exit, fatal, and signal paths flush the
15
+ * rest. `ZETA_LOG_LEVEL` optionally limits the levels written to the file
16
+ * (default: all).
15
17
  */
16
18
  import { AsyncLocalStorage } from "node:async_hooks";
17
19
  import * as fs from "node:fs";
@@ -48,7 +50,18 @@ export function registerLogSink(sink: LogSink): () => void {
48
50
 
49
51
  function emitToSinks(level: LogLevel, message: string, context: Record<string, unknown> | undefined): void {
50
52
  if (logSinks.size === 0) return;
51
- const event: LogEvent = { level, message, context, timestamp: new Date() };
53
+ // The enclosing {@link withContext} fields reach sinks as they reach the file;
54
+ // call-site fields win, and a record outside any context keeps its own object.
55
+ const scoped = logContextStorage.getStore();
56
+ let merged = context;
57
+ if (scoped) {
58
+ try {
59
+ merged = { ...scoped, ...context };
60
+ } catch {
61
+ // A throwing getter must not turn a log call into a failure; sinks get the call's own fields.
62
+ }
63
+ }
64
+ const event: LogEvent = { level, message, context: merged, timestamp: new Date() };
52
65
  for (const sink of logSinks) {
53
66
  try {
54
67
  sink(event);
@@ -251,6 +264,28 @@ function formatLocalTimestamp(date: Date): string {
251
264
 
252
265
  const FORMAT_TOKEN_PATTERN = /%[scdjifoO%]/;
253
266
 
267
+ const logContextStorage = new AsyncLocalStorage<Readonly<Record<string, unknown>>>();
268
+
269
+ /**
270
+ * Run `fn` with `context`'s fields on every record logged while it runs,
271
+ * including from the promises, timers and callbacks it starts. A nested call
272
+ * replaces the enclosing context, as a subagent's session replaces its
273
+ * parent's; a field passed to the log call itself wins. Fields are read as each
274
+ * record is written, so a getter stays current.
275
+ */
276
+ export function withContext<T>(context: Readonly<Record<string, unknown>>, fn: () => T): T {
277
+ return logContextStorage.run(context, fn);
278
+ }
279
+
280
+ /**
281
+ * Run `fn` outside any {@link withContext}: for process-wide work started on
282
+ * behalf of one caller, such as a shared server's read loop, whose later
283
+ * records belong to no single caller.
284
+ */
285
+ export function withoutContext<T>(fn: () => T): T {
286
+ return logContextStorage.exit(fn);
287
+ }
288
+
254
289
  /** Serialize one log record: timestamp, level, pid, message, then context fields. */
255
290
  function formatLogRecord(
256
291
  level: LogLevel,
@@ -266,6 +301,12 @@ function formatLogRecord(
266
301
  pid: process.pid,
267
302
  message: metadata?.message ? `${message} ${metadata.message}` : message,
268
303
  };
304
+ const scoped = logContextStorage.getStore();
305
+ if (scoped) {
306
+ for (const key of Object.keys(scoped)) {
307
+ if (key !== "level" && key !== "timestamp" && key !== "message") entry[key] = scoped[key];
308
+ }
309
+ }
269
310
  if (metadata) {
270
311
  for (const key of Object.keys(metadata)) {
271
312
  if (key !== "level" && key !== "timestamp" && key !== "message") entry[key] = metadata[key];
package/src/ports.ts CHANGED
@@ -3,8 +3,9 @@
3
3
  *
4
4
  * OMP installs on the same machine use 30141/30142/3847, and common developer
5
5
  * tools squat on 3000/8080/8787/3847. Zeta therefore owns a dedicated block:
6
- * three uncommon ports, at least two apart, so a side-by-side OMP install and
7
- * unrelated services cannot take the gateway or the stats dashboard away.
6
+ * uncommon ports, at least two apart, so a side-by-side OMP install and
7
+ * unrelated services cannot take the gateway, stats dashboard, or browser
8
+ * relay away.
8
9
  *
9
10
  * Every default in the repository derives from these constants. The standalone
10
11
  * Electron shell (`desktop/src/main.ts`, not a Bun workspace package) carries
@@ -13,3 +14,4 @@
13
14
  export const ZETA_WEB_PORT = 43141;
14
15
  export const ZETA_GATEWAY_PORT = 43143;
15
16
  export const ZETA_STATS_PORT = 43147;
17
+ export const ZETA_BROWSER_RELAY_PORT = 43153;
@@ -1,17 +0,0 @@
1
- /**
2
- * `fs.openSync` with close-on-exec, matching Node's default.
3
- *
4
- * libuv adds `O_CLOEXEC` inside `uv__fs_open`, so on Node every `fs` descriptor
5
- * dies at `exec`. Bun's `fs.open*` did not before 1.4.3 (still allowed by the
6
- * `bun@>=1.4` pin), and descriptors it returns survive
7
- * into any child spawned through a path that does not close strays itself — the
8
- * bash tool's shell `fork`/`exec`s user commands and hands them the lot. Use
9
- * this for every descriptor kept open past such a spawn (session transcripts,
10
- * logs, spools); a child must never hold a writable handle to them (#13224).
11
- *
12
- * Flags are numeric only: the bit cannot be OR-ed into `"a"`/`"w+"` strings.
13
- *
14
- * @example
15
- * const fd = openCloexecSync(logPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
16
- */
17
- export declare function openCloexecSync(filePath: string, flags: number, mode?: number): number;
package/src/fs-open.ts DELETED
@@ -1,32 +0,0 @@
1
- /** Close-on-exec file opens for descriptors held past a child spawn. */
2
- import * as fs from "node:fs";
3
-
4
- /**
5
- * `O_CLOEXEC`, which neither Node nor Bun exposes in `fs.constants`.
6
- *
7
- * Kernel ABI values: `0o2000000` on Linux, `0x0100_0000` on Darwin. Windows has
8
- * no `exec` and its CRT handles are inherited only when a spawn explicitly asks
9
- * for it, so the bit is 0 there — as it is on any other platform, where adding
10
- * an unknown flag bit would be worse than leaving the descriptor inheritable.
11
- */
12
- const O_CLOEXEC = process.platform === "linux" ? 0o2000000 : process.platform === "darwin" ? 0x0100_0000 : 0;
13
-
14
- /**
15
- * `fs.openSync` with close-on-exec, matching Node's default.
16
- *
17
- * libuv adds `O_CLOEXEC` inside `uv__fs_open`, so on Node every `fs` descriptor
18
- * dies at `exec`. Bun's `fs.open*` did not before 1.4.3 (still allowed by the
19
- * `bun@>=1.4` pin), and descriptors it returns survive
20
- * into any child spawned through a path that does not close strays itself — the
21
- * bash tool's shell `fork`/`exec`s user commands and hands them the lot. Use
22
- * this for every descriptor kept open past such a spawn (session transcripts,
23
- * logs, spools); a child must never hold a writable handle to them (#13224).
24
- *
25
- * Flags are numeric only: the bit cannot be OR-ed into `"a"`/`"w+"` strings.
26
- *
27
- * @example
28
- * const fd = openCloexecSync(logPath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND);
29
- */
30
- export function openCloexecSync(filePath: string, flags: number, mode?: number): number {
31
- return fs.openSync(filePath, flags | O_CLOEXEC, mode);
32
- }