@linxiraos/pi-utils 1.1.4 → 1.1.6

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,11 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [1.1.4] - 2026-08-26
5
+ ## [1.1.6] - 2026-08-30
6
6
 
7
- ### Changed
7
+ - 同步上游 OMP v18.0.10(`33cc6b9a043a`)。
8
+ - 同步上游 OMP v18.0.9(`cc14e04f075d`)。
9
+
10
+ ## [1.1.5] - 2026-08-26
8
11
 
9
- - 同步 1.1.4 发布线(与 1.1.3 无功能差异)。
12
+ - 同步上游 OMP v18.0.5 / v18.0.6:新增 browsers / json 工具,SHA-2/SHA-3 在 ARM64 上加速。
10
13
 
11
14
  ## [1.1.3] - 2026-08-25
12
15
 
package/README.md CHANGED
@@ -17,6 +17,7 @@ Shared utilities for [Zeta](https://github.com/Linxira-OS/linxira-zeta) packages
17
17
  | `fs-error` | Errno guards (`isEnoent` and friends) |
18
18
  | `env` / `worker-host` | Environment plumbing and side-effect-free worker-host entry contract (`workerHostEntry`) |
19
19
  | `abortable` / `async` | AbortSignal-aware stream/promise helpers |
20
+ | `math-delimiters` | LaTeX span/block delimiter grammar (offsets only) shared by the TUI and collab-web renderers |
20
21
  | `peek-file` | Read the first N bytes of a file with pooled buffers |
21
22
  | `frontmatter`, `glob`, `mime`, `temp`, `format`, `color`, `snowflake`, `tab-spacing`, `path-tree`, `sanitize-text` | Smaller single-purpose helpers |
22
23
 
@@ -55,6 +55,13 @@ export declare class RequestError extends Error {
55
55
  static authRequired(data?: unknown, additionalMessage?: string): RequestError;
56
56
  /** Creates a resource-not-found error. */
57
57
  static resourceNotFound(uri?: string): RequestError;
58
+ /**
59
+ * Creates a session-busy error: the agent/session is already processing, so the
60
+ * request can be retried once idle (steer/follow-up/wait) instead of treating it
61
+ * as a fault. `message` carries the caller's user-facing wording; `data` should
62
+ * keep the stable discriminator shape (`reason: "session_busy"`).
63
+ */
64
+ static sessionBusy(message: string, data?: unknown): RequestError;
58
65
  /** Converts this error into a JSON-RPC result. */
59
66
  toResult(): {
60
67
  error: ErrorResponse;
@@ -17,4 +17,6 @@ export declare class AsyncDrain<T> {
17
17
  constructor(delayMs?: number);
18
18
  /** Queue `value`; `hnd` receives the whole batch when the window closes. */
19
19
  push(value: T, hnd: (values: T[]) => Promise<void> | void): Promise<void>;
20
+ /** Runs the pending batch handler immediately and returns its completion promise. */
21
+ flush(): Promise<void>;
20
22
  }
@@ -82,6 +82,40 @@ export declare function adjustHsv(hex: string, adj: HSVAdjustment): string;
82
82
  * Convert HSL (h: 0-360, s: 0-1, l: 0-1) to a CSS hex string.
83
83
  */
84
84
  export declare function hslToHex(h: number, s: number, l: number): string;
85
+ export interface OKLCH {
86
+ /** Perceptual lightness (0-1) */
87
+ l: number;
88
+ /** Chroma (0 = gray; sRGB peaks around 0.37) */
89
+ c: number;
90
+ /** Hue in degrees (0-360) */
91
+ h: number;
92
+ }
93
+ /**
94
+ * Convert a hex color to OKLCH (perceptual lightness/chroma/hue).
95
+ *
96
+ * Unlike HSL, equal `l`/`c` values look equally bright and colorful across
97
+ * hues, so carrying them between colors preserves the palette's "weight".
98
+ */
99
+ export declare function hexToOklch(hex: string): OKLCH;
100
+ /**
101
+ * The sRGB gamut cusp for an OKLCH hue: the lightness/chroma point where the
102
+ * hue reaches its maximum chroma inside sRGB.
103
+ *
104
+ * The cusp lightness varies wildly per hue (yellow ≈ 0.97, blue ≈ 0.45), so
105
+ * transferring absolute OKLCH lightness/chroma between hues distorts
106
+ * vividness; normalize against the cusp instead. See `getSessionAccentHex`.
107
+ */
108
+ export declare function oklchCusp(h: number): {
109
+ l: number;
110
+ c: number;
111
+ };
112
+ /**
113
+ * Convert OKLCH to a CSS hex string, gamut-mapping by chroma reduction.
114
+ *
115
+ * Out-of-gamut inputs keep their lightness and hue while chroma is bisected
116
+ * down until the color fits sRGB, matching CSS Color 4's recommended intent.
117
+ */
118
+ export declare function oklchToHex(oklch: OKLCH): string;
85
119
  /**
86
120
  * Perceptual luma (gamma-encoded BT.709 weights over raw sRGB), normalized to 0..1.
87
121
  *
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Centralized path helpers for omp config directories.
2
+ * Centralized path helpers for zeta config directories.
3
3
  *
4
4
  * Uses PI_CONFIG_DIR (default ".zeta") for the config root and
5
5
  * PI_CODING_AGENT_DIR to override the agent directory.
6
6
  *
7
7
  * On Linux, if XDG_DATA_HOME / XDG_STATE_HOME / XDG_CACHE_HOME environment
8
8
  * variables are set, paths are redirected to XDG-compliant locations under
9
- * $XDG_*_HOME/omp/. This requires running `zeta config migrate` first to
9
+ * $XDG_*_HOME/zeta/. This requires running `zeta config migrate` first to
10
10
  * move data to the new locations. No filesystem existence checks are performed
11
11
  * — if the env var is set, omp trusts that the migration has been done.
12
12
  */
13
- /** App name (e.g. "zeta") */
13
+ /** App name (e.g. "omp") */
14
14
  export declare const APP_NAME: string;
15
15
  /** Config directory name (e.g. ".zeta") */
16
16
  export declare const CONFIG_DIR_NAME: string;
@@ -18,7 +18,7 @@ export declare const CONFIG_DIR_NAME: string;
18
18
  export declare const MAIN_CONFIG_FILENAMES: readonly ["config.yml", "config.yaml"];
19
19
  /** Version (e.g. "1.0.0") */
20
20
  export declare const VERSION: string;
21
- /** Default User-Agent header string (e.g. "zeta/1.0.10") */
21
+ /** Default User-Agent header string (e.g. "zeta/1.1.5") */
22
22
  export declare const USER_AGENT: string;
23
23
  /** Minimum Bun version */
24
24
  export declare const MIN_BUN_VERSION: string;
@@ -48,6 +48,10 @@ export declare function relativePathWithinRoot(root: string, candidate: string):
48
48
  export declare function getProjectDir(): string;
49
49
  /** Set the project directory. */
50
50
  export declare function setProjectDir(dir: string): void;
51
+ /** Reset the cached project directory (test seam). */
52
+ export declare function __resetProjectDirCacheForTests(): void;
53
+ /** Whether a path is absent or not a directory. Other stat failures return false. */
54
+ export declare function directoryIsMissing(dir: string): Promise<boolean>;
51
55
  /**
52
56
  * Whether `dir` resolves to an existing directory. Any stat failure — a deleted
53
57
  * path (ENOENT), permission error, or a non-directory — returns `false`, so
@@ -55,16 +59,23 @@ export declare function setProjectDir(dir: string): void;
55
59
  * working directory before {@link setProjectDir} throws on it.
56
60
  */
57
61
  export declare function directoryExists(dir: string): Promise<boolean>;
58
- /** Get the config directory name relative to home (e.g. ".zeta" or PI_CONFIG_DIR override). */
59
- export declare function getConfigDirName(): string;
60
62
  /**
61
- * One-time migration of a legacy `~/.omp` config root to `~/.zeta`. Called
62
- * from the CLI entrypoint before any user data is written; it only moves the
63
- * directory when `.zeta` does not exist yet. Zeta keeps no `.omp`
64
- * compatibility surface afterwards (see AGENTS.md), so this is the single
65
- * exception that honors pre-existing installs.
63
+ * Whether `dir` both exists and can be entered. POSIX `stat` succeeds for a
64
+ * directory whose own search/execute permission is denied (it only needs
65
+ * +x on the parent chain), so existence alone does not imply `chdir` works.
66
+ * Callers that adopt a directory as a working directory must check this
67
+ * rather than {@link directoryExists} alone.
66
68
  */
67
- export declare function migrateLegacyOmpConfigDir(): void;
69
+ export declare function directoryIsEnterable(dir: string): Promise<boolean>;
70
+ /** Whether `dir` is enterable, synchronous variant. See {@link directoryIsEnterable}. */
71
+ export declare function directoryIsEnterableSync(dir: string): boolean;
72
+ /**
73
+ * Project directory when it is enterable, otherwise a safe fallback.
74
+ * Used by spawns that must preserve project-relative behavior when healthy.
75
+ */
76
+ export declare function getSafeProjectCwd(): string;
77
+ /** Get the config directory name relative to home (e.g. ".zeta" or PI_CONFIG_DIR override). */
78
+ export declare function getConfigDirName(): string;
68
79
  /** Get the config agent directory name relative to home (e.g. ".zeta/agent" or PI_CONFIG_DIR + "/agent"). */
69
80
  export declare function getConfigAgentDirName(): string;
70
81
  /**
@@ -112,7 +123,7 @@ export declare function getProjectAgentDir(cwd?: string): string;
112
123
  export declare function getReportsDir(): string;
113
124
  /** Get the logs directory (~/.zeta/logs). */
114
125
  export declare function getLogsDir(): string;
115
- /** Get this process's dated log path (~/.zeta/logs/zeta.YYYY-MM-DD.PID.log). */
126
+ /** Get this process's dated log path (~/.zeta/logs/omp.YYYY-MM-DD.PID.log). */
116
127
  export declare function getLogPath(date?: Date, pid?: number): string;
117
128
  /**
118
129
  * Get the plugins directory (~/.zeta/plugins or its XDG equivalent).
@@ -129,13 +140,13 @@ export declare function getPluginsDir(home?: string): string;
129
140
  export declare function getPluginsNodeModules(home?: string): string;
130
141
  /** Plugin manifest (~/.zeta/plugins/package.json). */
131
142
  export declare function getPluginsPackageJson(home?: string): string;
132
- /** Plugin lock file (~/.zeta/plugins/zeta-plugins.lock.json). */
143
+ /** Plugin lock file (~/.zeta/plugins/omp-plugins.lock.json). */
133
144
  export declare function getPluginsLockfile(home?: string): string;
134
145
  /** Get the remote mount directory (~/.zeta/remote). */
135
146
  export declare function getRemoteDir(): string;
136
147
  /**
137
148
  * Relocate the base directory for agent-managed worktrees (PR checkouts, task
138
- * isolation, and `zeta worktree` cleanup all read the same base). Driven by the
149
+ * isolation, and `omp worktree` cleanup all read the same base). Driven by the
139
150
  * `worktree.base` setting in coding-agent; pass `undefined`/empty to clear and
140
151
  * fall back to `OMP_WORKTREE_DIR` or the `~/.zeta/wt` default.
141
152
  *
@@ -189,6 +200,11 @@ export declare function getGpuCachePath(): string;
189
200
  * cache file without touching the rest of the config root.
190
201
  */
191
202
  export declare function getGithubCacheDbPath(): string;
203
+ /**
204
+ * Get the conventional commit inference cache database path (~/.zeta/cache/commit-inference.db).
205
+ * Honors `OMP_COMMIT_CACHE_DB` so tests and operators can isolate the cache.
206
+ */
207
+ export declare function getCommitCacheDbPath(): string;
192
208
  /** Get the legacy Pi extension parse cache database path. */
193
209
  export declare function getLegacyPiExtensionCacheDbPath(): string;
194
210
  /**
@@ -197,10 +213,9 @@ export declare function getLegacyPiExtensionCacheDbPath(): string;
197
213
  * operators can isolate or relocate the cache file.
198
214
  */
199
215
  export declare function getAuthBrokerSnapshotCachePath(): string;
200
- /** Get the local FastEmbed model cache directory (~/.zeta/cache/fastembed). */
201
- /** Get the commit-author avatar cache directory (~/.omp/cache/avatars). */
216
+ /** Get the commit-author avatar cache directory (~/.zeta/cache/avatars). */
202
217
  export declare function getAvatarCacheDir(): string;
203
- /** Get the local FastEmbed model cache directory (~/.omp/cache/fastembed). */
218
+ /** Get the local FastEmbed model cache directory (~/.zeta/cache/fastembed). */
204
219
  export declare function getFastembedCacheDir(): string;
205
220
  /** Get the on-demand fastembed runtime install root (~/.zeta/cache/fastembed-runtime). */
206
221
  export declare function getFastembedRuntimeDir(): string;
@@ -232,6 +247,8 @@ export declare function getModelDbPath(agentDir?: string): string;
232
247
  export declare function getTinyModelsCacheDir(agentDir?: string): string;
233
248
  /** Get the document conversion cache directory (~/.zeta/agent/cache/document-conversions; XDG default: $XDG_CACHE_HOME/zeta/cache/document-conversions). */
234
249
  export declare function getDocumentConversionCacheDir(agentDir?: string): string;
250
+ /** Get the per-project composer speculative cache directory (~/.zeta/agent/cache/composer; XDG default: $XDG_CACHE_HOME/zeta/cache/composer). */
251
+ export declare function getComposerCacheDir(agentDir?: string): string;
235
252
  /** Get the sessions directory (~/.zeta/agent/sessions). */
236
253
  export declare function getSessionsDir(agentDir?: string): string;
237
254
  /** Get the content-addressed blob store directory (~/.zeta/agent/blobs). */
@@ -250,9 +267,9 @@ export declare function getAgentModulesDir(agentDir?: string): string;
250
267
  export declare function getMemoriesDir(agentDir?: string): string;
251
268
  /** Get the terminal sessions directory (~/.zeta/agent/terminal-sessions). */
252
269
  export declare function getTerminalSessionsDir(agentDir?: string): string;
253
- /** Get the crash log path (~/.zeta/agent/zeta-crash.log). */
270
+ /** Get the crash log path (~/.zeta/agent/omp-crash.log). */
254
271
  export declare function getCrashLogPath(agentDir?: string): string;
255
- /** Get the debug log path (~/.zeta/agent/zeta-debug.log). */
272
+ /** Get the debug log path (~/.zeta/agent/omp-debug.log). */
256
273
  export declare function getDebugLogPath(agentDir?: string): string;
257
274
  /** Get the secret placeholder key path (~/.zeta/agent/secret-placeholder.key; XDG default: $XDG_STATE_HOME/zeta/secret-placeholder.key). Adopts a legacy key on first XDG resolution. */
258
275
  export declare function getSecretPlaceholderKeyPath(): string;
@@ -278,10 +295,13 @@ export declare function getProjectPluginOverridesPath(cwd?: string): string;
278
295
  export declare function getMCPConfigPath(scope: "user" | "project", cwd?: string): string;
279
296
  /** Get the SSH config file path. */
280
297
  export declare function getSSHConfigPath(scope: "user" | "project", cwd?: string): string;
281
- /** Get the project-level tracking directory (<project>/.zeta/tracking). */
282
- export declare function getProjectTrackingDir(cwd?: string): string;
283
- /** Get the global tracking index path (~/.zeta/agent/tracking-index.json). */
284
- export declare function getTrackingIndexPath(agentDir?: string): string;
298
+ /**
299
+ * Application label for usage attribution (`OMP_APP_NAME`), defaulting to
300
+ * `omp`. Embedders that drive omp programmatically (robomp, CI bots, …) set
301
+ * the env var so broker-side per-client burn tracking can answer "what did
302
+ * app X use" instead of folding everything into one install-wide bucket.
303
+ */
304
+ export declare function getAppName(): string;
285
305
  /**
286
306
  * Persistent per-install UUID stored at `~/.zeta/install-id`.
287
307
  *
@@ -299,3 +319,7 @@ export declare function getTrackingIndexPath(agentDir?: string): string;
299
319
  export declare function getInstallId(): string;
300
320
  /** Test-only: clear cached install id. Never call from production code. */
301
321
  export declare function __resetInstallIdCacheForTests(): void;
322
+ /** Get the project-level tracking directory (<project>/.zeta/tracking). */
323
+ export declare function getProjectTrackingDir(cwd?: string): string;
324
+ /** Get the global tracking index path (~/.zeta/agent/tracking-index.json). */
325
+ export declare function getTrackingIndexPath(agentDir?: string): string;
@@ -14,6 +14,7 @@ export * from "./json.js";
14
14
  export * from "./json-parse.js";
15
15
  export * as logger from "./logger.js";
16
16
  export * from "./loop-phase.js";
17
+ export * from "./math-delimiters.js";
17
18
  export * from "./mermaid-ascii.js";
18
19
  export * from "./mime.js";
19
20
  export * from "./path.js";
@@ -12,3 +12,9 @@ export declare function tryParseJson<T = unknown>(content: string): T | null;
12
12
  * only lossless JSON representation.
13
13
  */
14
14
  export declare function stringifyJson(value: unknown, space?: string | number): string | undefined;
15
+ /**
16
+ * Deterministically serialize JSON-shaped data by sorting object keys at every
17
+ * depth while preserving array order. Throws for values JSON cannot represent
18
+ * as a top-level value instead of returning an easy-to-misuse undefined.
19
+ */
20
+ export declare function stableStringifyJson(value: unknown): string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * LaTeX delimiter grammar for agent-authored Markdown: where does a math span
3
+ * begin and end, in source offsets. Carries no rendering policy — what to do
4
+ * with an unclosed opener, whether a body is typesettable, and how it is
5
+ * displayed belong to the renderer (Unicode in the TUI, KaTeX in collab web).
6
+ */
7
+ /** Opening delimiter. Each closer (`$`, `$$`, `\)`, `\]`) is as wide as its opener. */
8
+ export type MathOpener = "$" | "$$" | "\\(" | "\\[";
9
+ /** A closed math span found in the source. */
10
+ export interface MathSpan {
11
+ opener: MathOpener;
12
+ /** True for the display forms `$$…$$` and `\[…\]`. */
13
+ display: boolean;
14
+ /** Offset one past the closing delimiter. */
15
+ end: number;
16
+ /** Source between the delimiters, verbatim. */
17
+ body: string;
18
+ }
19
+ /** An own-line display block: opener and closer each alone on their line. */
20
+ export interface MathBlock {
21
+ /** Both delimiter lines, the body, and the trailing newline. */
22
+ raw: string;
23
+ body: string;
24
+ }
25
+ /**
26
+ * Leftmost offset at or after `from` where an opener could begin. A scan hint,
27
+ * not a decision: whether that candidate is really math — escaped, currency,
28
+ * unclosed — is decided by {@link mathSpanAt}.
29
+ */
30
+ export declare function mathStartIndex(source: string, from?: number): number | undefined;
31
+ /** Math opener at `at`, or `undefined` when no delimiter starts there. */
32
+ export declare function mathOpenerAt(source: string, at: number): MathOpener | undefined;
33
+ /**
34
+ * The span opened at `at`, or `undefined` when the run is not math — including
35
+ * an opener the source escaped, so `\$x$` and `\\(x\)` are literal text.
36
+ *
37
+ * `from` bounds how far back the escape scan may look. Leave it at 0 when
38
+ * reading raw source. Pass the offset your own walk resumed at if you have
39
+ * already consumed the escapes behind it, as `renderMathInText` does: after it
40
+ * emits the `\\` of `\\\(x\)`, the `\(` that follows is a real opener even
41
+ * though a backslash precedes it.
42
+ */
43
+ export declare function mathSpanAt(source: string, at: number, from?: number): MathSpan | undefined;
44
+ /** The own-line display block starting at offset 0, or `undefined`. */
45
+ export declare function mathBlockAt(source: string): MathBlock | undefined;
@@ -44,6 +44,19 @@ export type BrokenPipeSource = "ipc-send" | "stdio-write";
44
44
  export declare function classifyBrokenPipe(err: Error): BrokenPipeSource | undefined;
45
45
  /** Whether an EPIPE came from an IPC `send()` to an optional worker. */
46
46
  export declare function isIpcSendEpipe(err: Error): boolean;
47
+ /**
48
+ * Whether an uncaught error is Bun's asynchronous `ERR_SOCKET_CLOSED` thrown
49
+ * from inside `node:net` internals with no application frames on the stack.
50
+ *
51
+ * Bun ≥1.4 can fire the close callback of an already-closed `node:net` socket
52
+ * on a fresh stack; the throw bypasses every callsite try/catch and surfaces
53
+ * here as a process-level uncaughtException. Closing an already-closed socket
54
+ * is inherently a no-op — the socket owner's own `error`/`close` handlers
55
+ * still drive recovery — so tearing the session down for it is pure loss.
56
+ * Only frameless internal stacks qualify: an `ERR_SOCKET_CLOSED` raised
57
+ * through application code keeps the fatal path.
58
+ */
59
+ export declare function isInternalSocketClosedError(err: unknown): boolean;
47
60
  /**
48
61
  * Detect Bun's advanced-serialization (structured-clone) IPC decode failure.
49
62
  *
@@ -94,10 +107,10 @@ export declare function registerStdioDisconnectHandling(): () => void;
94
107
  */
95
108
  export declare function markExpectedCleanupError<T extends object>(reason: T): T;
96
109
  /**
97
- * Whether `reason` (or any error in its `cause` chain) was marked via
98
- * {@link markExpectedCleanupError}. Walks the chain because the unhandled
99
- * reason is often a wrapper (`AbortError`) with the marked abort reason as
100
- * its `cause`.
110
+ * Whether `reason` (or any object in its bounded `cause` chain) was explicitly
111
+ * marked via {@link markExpectedCleanupError}. Runtime error names and codes
112
+ * are intentionally insufficient: unmarked `AbortError` and socket failures
113
+ * can originate from application code and must remain fatal when unhandled.
101
114
  */
102
115
  export declare function isExpectedCleanupError(reason: unknown): boolean;
103
116
  /**
@@ -110,15 +123,33 @@ export declare function interceptUnhandledRejections(interceptor: (reason: unkno
110
123
  * through an uncaught exception or unhandled rejection.
111
124
  */
112
125
  export declare function registerFatalRecoveryHint(provider: FatalRecoveryHintProvider): () => void;
126
+ /** Controls when a registered cleanup callback participates in cleanup passes. */
127
+ export interface CleanupRegistrationOptions {
128
+ /**
129
+ * Run only on a real exit, never during a manual keep-alive cleanup.
130
+ * The registration remains armed when a keep-alive pass skips it.
131
+ */
132
+ exitOnly?: boolean;
133
+ }
113
134
  /**
114
- * Register a process cleanup callback, to be run on shutdown, signal, or fatal error.
135
+ * Registers a cleanup callback for shutdown, signals, fatal errors, and
136
+ * repeatable manual cleanup passes.
115
137
  *
116
- * Returns a Callback instance that can be used to cancel (unregister) or manually clean up.
117
- * If register is called after cleanup already began, invokes callback on a microtask.
138
+ * Registrations persist across keep-alive {@link cleanup} passes and run at
139
+ * most once per pass. Set `exitOnly` for resources the continuing process still
140
+ * holds (open databases, cached handles): keep-alive passes skip the callback
141
+ * without consuming its registration, while the eventual real exit runs it.
142
+ *
143
+ * A callback registered during a running keep-alive pass joins future passes;
144
+ * normal callbacks also run immediately for the current pass. Registrations
145
+ * made during a real exit run immediately.
146
+ *
147
+ * Returns a function that permanently cancels the registration.
118
148
  */
119
- export declare function register(id: string, callback: (reason: Reason) => void | Promise<void>): () => void;
149
+ export declare function register(id: string, callback: (reason: Reason) => void | Promise<void>, options?: CleanupRegistrationOptions): () => void;
120
150
  /**
121
- * Runs all cleanup callbacks without exiting.
151
+ * Runs all cleanup callbacks without exiting, then re-arms the system so
152
+ * resources opened afterwards are still cleaned at the eventual real exit.
122
153
  * Use this in workers or when you need to clean up but continue execution.
123
154
  */
124
155
  export declare function cleanup(): Promise<void>;
@@ -127,6 +158,12 @@ export interface QuitOptions {
127
158
  /** Wait for buffered stdout before exiting; disable after the terminal has disconnected. */
128
159
  drainStdout?: boolean;
129
160
  }
161
+ /**
162
+ * Waits (bounded) for buffered stdout to reach the terminal. Used before
163
+ * process exit and before an exec-replace, where unflushed output would be
164
+ * lost with the process image.
165
+ */
166
+ export declare function drainStdout(): Promise<void>;
130
167
  /**
131
168
  * Runs all cleanup callbacks and exits through the current `process.exit`.
132
169
  *
@@ -10,6 +10,13 @@ import type { Spawn, Subprocess } from "bun";
10
10
  type InMask = "pipe" | "ignore" | Buffer | Uint8Array | null;
11
11
  /** A Bun subprocess with stdout/stderr always piped (stdin may vary). */
12
12
  type PipedSubprocess<In extends InMask = InMask> = Subprocess<In, "pipe", "pipe">;
13
+ /**
14
+ * Build the Linux child-subreaper entrypoint.
15
+ *
16
+ * @internal Exported so tests can force a missing first libc soname and verify
17
+ * the loader continues to the next candidate.
18
+ */
19
+ export declare function createLinuxSubreaperScript(libcCandidates?: readonly string[]): string;
13
20
  /**
14
21
  * Base for all exceptions representing child process nonzero exit, killed, or
15
22
  * cancellation.
@@ -63,7 +70,7 @@ export declare class ChildProcess<In extends InMask = InMask> {
63
70
  #private;
64
71
  readonly proc: PipedSubprocess<In>;
65
72
  readonly exposeStderr: boolean;
66
- constructor(proc: PipedSubprocess<In>, exposeStderr: boolean, retainFullStderr?: boolean);
73
+ constructor(proc: PipedSubprocess<In>, exposeStderr: boolean, retainFullStderr?: boolean, terminateGroup?: boolean, hardKillTree?: boolean);
67
74
  get pid(): number;
68
75
  get exited(): Promise<number>;
69
76
  get exitCode(): number | null;
@@ -93,6 +100,13 @@ export declare class ChildProcess<In extends InMask = InMask> {
93
100
  type ChildSpawnOptions<In extends InMask = InMask> = Omit<Spawn.SpawnOptions<In, "pipe", "pipe">, "stdout" | "stderr" | "detached"> & {
94
101
  signal?: AbortSignal;
95
102
  detached?: boolean;
103
+ /**
104
+ * On Linux, supervise the command from a child subreaper so descendants
105
+ * remain reachable after changing session and reparenting. Other platforms
106
+ * ignore this option. macOS process groups cannot retain a daemonized
107
+ * descendant that creates a new session and reparents to launchd.
108
+ */
109
+ subreaper?: boolean;
96
110
  /** Expose and retain complete stderr for a later `wait({ stderr: "full" })`. */
97
111
  stderr?: "full" | null;
98
112
  };
@@ -7,6 +7,9 @@
7
7
  * one implementation here prevents the classifiers from drifting between the
8
8
  * credential store and the model cache.
9
9
  */
10
+ import type { Database } from "bun:sqlite";
11
+ /** Checkpoints committed WAL frames without waiting for concurrent readers. */
12
+ export declare function checkpointWal(db: Database): void;
10
13
  /**
11
14
  * SQLite's busy result-code family — base `SQLITE_BUSY` plus the extended
12
15
  * variants `SQLITE_BUSY_RECOVERY` (concurrent WAL recovery), `SQLITE_BUSY_SNAPSHOT`,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@linxiraos/pi-utils",
4
- "version": "1.1.4",
4
+ "version": "1.1.6",
5
5
  "description": "Shared utilities for pi packages",
6
6
  "homepage": "https://linxira-os.github.io/zeta/",
7
7
  "author": "Can Boluk",
@@ -31,7 +31,7 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@linxiraos/pi-natives": "1.1.4"
34
+ "@linxiraos/pi-natives": "1.1.6"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/bun": "^1.3.14"
@@ -77,6 +77,15 @@ export class RequestError extends Error {
77
77
  uri === undefined ? undefined : { uri },
78
78
  );
79
79
  }
80
+ /**
81
+ * Creates a session-busy error: the agent/session is already processing, so the
82
+ * request can be retried once idle (steer/follow-up/wait) instead of treating it
83
+ * as a fault. `message` carries the caller's user-facing wording; `data` should
84
+ * keep the stable discriminator shape (`reason: "session_busy"`).
85
+ */
86
+ static sessionBusy(message: string, data?: unknown): RequestError {
87
+ return new RequestError(-32003, message, data);
88
+ }
80
89
  /** Converts this error into a JSON-RPC result. */
81
90
  toResult(): { error: ErrorResponse } {
82
91
  return { error: this.toErrorResponse() };
package/src/async.ts CHANGED
@@ -59,6 +59,7 @@ export function withTimeout<T>(promise: Promise<T>, ms: number, message: string,
59
59
  export class AsyncDrain<T> {
60
60
  #queue?: T[];
61
61
  #promise = Promise.resolve();
62
+ #flush?: () => void;
62
63
 
63
64
  constructor(readonly delayMs: number = 0) {}
64
65
 
@@ -66,21 +67,28 @@ export class AsyncDrain<T> {
66
67
  push(value: T, hnd: (values: T[]) => Promise<void> | void): Promise<void> {
67
68
  let queue = this.#queue;
68
69
  if (!queue) {
69
- this.#queue = queue = [];
70
+ const batch: T[] = [];
71
+ this.#queue = batch;
72
+ queue = batch;
70
73
  const { promise, resolve, reject } = Promise.withResolvers<void>();
71
74
  const exec = (): void => {
75
+ if (this.#queue !== batch) return;
76
+ this.#queue = undefined;
77
+ this.#flush = undefined;
72
78
  try {
73
- if (this.#queue === queue) {
74
- this.#queue = undefined;
75
- }
76
- resolve(hnd(queue!));
79
+ resolve(hnd(batch));
77
80
  } catch (error) {
78
81
  reject(error);
79
82
  }
80
83
  };
81
84
  if (this.delayMs > 0) {
82
- setTimeout(exec, this.delayMs);
85
+ const timer = setTimeout(exec, this.delayMs);
86
+ this.#flush = () => {
87
+ clearTimeout(timer);
88
+ exec();
89
+ };
83
90
  } else {
91
+ this.#flush = exec;
84
92
  queueMicrotask(exec);
85
93
  }
86
94
  this.#promise = promise;
@@ -88,4 +96,10 @@ export class AsyncDrain<T> {
88
96
  queue.push(value);
89
97
  return this.#promise;
90
98
  }
99
+
100
+ /** Runs the pending batch handler immediately and returns its completion promise. */
101
+ flush(): Promise<void> {
102
+ this.#flush?.();
103
+ return this.#promise;
104
+ }
91
105
  }
package/src/browsers.ts CHANGED
@@ -4,11 +4,23 @@ import type * as fs from "node:fs";
4
4
  import * as fsp from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
- import { extractArchive } from "./ar";
7
+ import { type ArchiveLimits, extractArchive } from "./ar";
8
8
 
9
9
  const CHROME_FOR_TESTING_BASE_URL = "https://storage.googleapis.com/chrome-for-testing-public";
10
10
  const CHROME_METADATA_BASE_URL = "https://googlechromelabs.github.io/chrome-for-testing";
11
11
 
12
+ /**
13
+ * Archive ceilings for the managed browser download. The default archive
14
+ * limits (64 MiB per member / 256 MiB in memory) guard against attacker-
15
+ * controlled archives, but the Chrome-for-Testing binary is a trusted
16
+ * first-party download whose `chrome` executable already exceeds both
17
+ * (~269 MB and growing), so extraction gets a ceiling sized for it.
18
+ */
19
+ const BROWSER_ARCHIVE_LIMITS: Partial<ArchiveLimits> = {
20
+ maxMemberSize: 1024 * 1024 * 1024,
21
+ maxInMemorySize: 1024 * 1024 * 1024,
22
+ };
23
+
12
24
  /** Supported browser products. */
13
25
  export enum Browser {
14
26
  CHROME = "chrome",
@@ -242,7 +254,7 @@ export async function install(options: InstallOptions): Promise<InstalledBrowser
242
254
  archivePath,
243
255
  options.downloadProgressCallback,
244
256
  );
245
- await extractArchive(archivePath, stagingPath);
257
+ await extractArchive(archivePath, stagingPath, { limits: BROWSER_ARCHIVE_LIMITS });
246
258
  await fsp.mkdir(path.dirname(installPath), { recursive: true });
247
259
  await fsp.rm(installPath, { recursive: true, force: true });
248
260
  await fsp.rename(stagingPath, installPath);