@linxiraos/pi-utils 1.1.5 → 1.1.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
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.1.7] - 2026-09-01
6
+
7
+ - 同步上游 OMP v18.0.11(`b8ce33a58911c26bed1d84f0db9a5e2e727c49a2`)。
8
+
9
+ ## [1.1.6] - 2026-08-30
10
+
11
+ - 同步上游 OMP v18.0.10(`33cc6b9a043a`)。
12
+ - 同步上游 OMP v18.0.9(`cc14e04f075d`)。
13
+
5
14
  ## [1.1.5] - 2026-08-26
6
15
 
7
16
  - 同步上游 OMP v18.0.5 / v18.0.6:新增 browsers / json 工具,SHA-2/SHA-3 在 ARM64 上加速。
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
  *
@@ -190,7 +201,7 @@ export declare function getGpuCachePath(): string;
190
201
  */
191
202
  export declare function getGithubCacheDbPath(): string;
192
203
  /**
193
- * Get the conventional commit inference cache database path (~/.omp/cache/commit-inference.db).
204
+ * Get the conventional commit inference cache database path (~/.zeta/cache/commit-inference.db).
194
205
  * Honors `OMP_COMMIT_CACHE_DB` so tests and operators can isolate the cache.
195
206
  */
196
207
  export declare function getCommitCacheDbPath(): string;
@@ -202,10 +213,9 @@ export declare function getLegacyPiExtensionCacheDbPath(): string;
202
213
  * operators can isolate or relocate the cache file.
203
214
  */
204
215
  export declare function getAuthBrokerSnapshotCachePath(): string;
205
- /** Get the local FastEmbed model cache directory (~/.zeta/cache/fastembed). */
206
- /** Get the commit-author avatar cache directory (~/.omp/cache/avatars). */
216
+ /** Get the commit-author avatar cache directory (~/.zeta/cache/avatars). */
207
217
  export declare function getAvatarCacheDir(): string;
208
- /** Get the local FastEmbed model cache directory (~/.omp/cache/fastembed). */
218
+ /** Get the local FastEmbed model cache directory (~/.zeta/cache/fastembed). */
209
219
  export declare function getFastembedCacheDir(): string;
210
220
  /** Get the on-demand fastembed runtime install root (~/.zeta/cache/fastembed-runtime). */
211
221
  export declare function getFastembedRuntimeDir(): string;
@@ -237,6 +247,8 @@ export declare function getModelDbPath(agentDir?: string): string;
237
247
  export declare function getTinyModelsCacheDir(agentDir?: string): string;
238
248
  /** Get the document conversion cache directory (~/.zeta/agent/cache/document-conversions; XDG default: $XDG_CACHE_HOME/zeta/cache/document-conversions). */
239
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;
240
252
  /** Get the sessions directory (~/.zeta/agent/sessions). */
241
253
  export declare function getSessionsDir(agentDir?: string): string;
242
254
  /** Get the content-addressed blob store directory (~/.zeta/agent/blobs). */
@@ -255,9 +267,9 @@ export declare function getAgentModulesDir(agentDir?: string): string;
255
267
  export declare function getMemoriesDir(agentDir?: string): string;
256
268
  /** Get the terminal sessions directory (~/.zeta/agent/terminal-sessions). */
257
269
  export declare function getTerminalSessionsDir(agentDir?: string): string;
258
- /** Get the crash log path (~/.zeta/agent/zeta-crash.log). */
270
+ /** Get the crash log path (~/.zeta/agent/omp-crash.log). */
259
271
  export declare function getCrashLogPath(agentDir?: string): string;
260
- /** Get the debug log path (~/.zeta/agent/zeta-debug.log). */
272
+ /** Get the debug log path (~/.zeta/agent/omp-debug.log). */
261
273
  export declare function getDebugLogPath(agentDir?: string): string;
262
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. */
263
275
  export declare function getSecretPlaceholderKeyPath(): string;
@@ -283,10 +295,13 @@ export declare function getProjectPluginOverridesPath(cwd?: string): string;
283
295
  export declare function getMCPConfigPath(scope: "user" | "project", cwd?: string): string;
284
296
  /** Get the SSH config file path. */
285
297
  export declare function getSSHConfigPath(scope: "user" | "project", cwd?: string): string;
286
- /** Get the project-level tracking directory (<project>/.zeta/tracking). */
287
- export declare function getProjectTrackingDir(cwd?: string): string;
288
- /** Get the global tracking index path (~/.zeta/agent/tracking-index.json). */
289
- 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;
290
305
  /**
291
306
  * Persistent per-install UUID stored at `~/.zeta/install-id`.
292
307
  *
@@ -304,3 +319,7 @@ export declare function getTrackingIndexPath(agentDir?: string): string;
304
319
  export declare function getInstallId(): string;
305
320
  /** Test-only: clear cached install id. Never call from production code. */
306
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";
@@ -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
  };
@@ -73,6 +73,13 @@ export interface EnsureRuntimeInstalledOptions {
73
73
  export declare function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void>;
74
74
  /**
75
75
  * Materialize a pinned dependency set into `runtimeDir` (idempotent,
76
- * cross-process safe via a lock directory). Returns `runtimeDir`.
76
+ * cross-process safe). Returns `runtimeDir`.
77
+ *
78
+ * Serialization uses the OS-backed {@link withFileLock} at
79
+ * `${runtimeDir}.install.lock`, which the kernel releases on process death, so
80
+ * a crashed installer cannot wedge later attempts (issue #10120). The path is
81
+ * deliberately distinct from the legacy `${runtimeDir}.lock` mkdir directory;
82
+ * {@link withLegacyInstallLock} atomically reserves that namespace during the
83
+ * new install so older processes cannot cross the migration boundary.
77
84
  */
78
85
  export declare function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string>;
@@ -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.5",
4
+ "version": "1.1.7",
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.5"
34
+ "@linxiraos/pi-natives": "1.1.7"
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
  }