@linxiraos/pi-utils 1.0.1 → 1.0.2
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 +36 -6
- package/package.json +56 -57
- package/src/dirs.ts +3 -0
- package/src/fetch-retry.ts +11 -2
- package/src/json-parse.ts +18 -7
- package/src/logger.ts +51 -16
- package/src/ptree.ts +2 -2
- package/src/stream.ts +27 -3
- package/src/vendor/mermaid-ascii/ascii/canvas.ts +8 -6
- package/src/vendor/mermaid-ascii/ascii/draw.ts +21 -14
- package/src/vendor/mermaid-ascii/text-metrics.ts +3 -0
- package/dist/types/abortable.d.ts +0 -32
- package/dist/types/acp/connection.d.ts +0 -118
- package/dist/types/acp/protocol.d.ts +0 -526
- package/dist/types/acp/schema.d.ts +0 -41
- package/dist/types/acp/stream.d.ts +0 -8
- package/dist/types/acp/transport.d.ts +0 -81
- package/dist/types/acp.d.ts +0 -6
- package/dist/types/async.d.ts +0 -20
- package/dist/types/binary.d.ts +0 -17
- package/dist/types/browsers.d.ts +0 -68
- package/dist/types/chalk.d.ts +0 -125
- package/dist/types/cli.d.ts +0 -134
- package/dist/types/color.d.ts +0 -102
- package/dist/types/dates.d.ts +0 -7
- package/dist/types/dirs.d.ts +0 -290
- package/dist/types/docx/converter.d.ts +0 -46
- package/dist/types/docx/xml.d.ts +0 -26
- package/dist/types/docx/zip.d.ts +0 -6
- package/dist/types/docx.d.ts +0 -11
- package/dist/types/dom/core.d.ts +0 -431
- package/dist/types/dom/parser.d.ts +0 -7
- package/dist/types/dom/selector.d.ts +0 -5
- package/dist/types/dom.d.ts +0 -5
- package/dist/types/env.d.ts +0 -124
- package/dist/types/fetch-retry.d.ts +0 -96
- package/dist/types/file-lock.d.ts +0 -23
- package/dist/types/format.d.ts +0 -37
- package/dist/types/frontmatter.d.ts +0 -46
- package/dist/types/fs-error.d.ts +0 -31
- package/dist/types/glob.d.ts +0 -28
- package/dist/types/headers.d.ts +0 -34
- package/dist/types/index.d.ts +0 -39
- package/dist/types/json-parse.d.ts +0 -74
- package/dist/types/json.d.ts +0 -14
- package/dist/types/logger/rotating-file.d.ts +0 -18
- package/dist/types/logger.d.ts +0 -96
- package/dist/types/loop-phase.d.ts +0 -10
- package/dist/types/lru.d.ts +0 -46
- package/dist/types/marked/core.d.ts +0 -445
- package/dist/types/marked.d.ts +0 -2
- package/dist/types/mermaid-ascii.d.ts +0 -11
- package/dist/types/mime.d.ts +0 -29
- package/dist/types/module-timer.d.ts +0 -1
- package/dist/types/path-tree.d.ts +0 -76
- package/dist/types/path.d.ts +0 -2
- package/dist/types/peek-file.d.ts +0 -29
- package/dist/types/postmortem.d.ts +0 -104
- package/dist/types/process-name.d.ts +0 -7
- package/dist/types/procmgr.d.ts +0 -74
- package/dist/types/prompt.d.ts +0 -18
- package/dist/types/ptree.d.ts +0 -110
- package/dist/types/readability/readability.d.ts +0 -9
- package/dist/types/readability/readerable.d.ts +0 -10
- package/dist/types/readability/types.d.ts +0 -70
- package/dist/types/readability.d.ts +0 -4
- package/dist/types/ring.d.ts +0 -93
- package/dist/types/runtime-install.d.ts +0 -78
- package/dist/types/sanitize-text.d.ts +0 -29
- package/dist/types/snowflake.d.ts +0 -25
- package/dist/types/stderr-guard.d.ts +0 -22
- package/dist/types/stream.d.ts +0 -67
- package/dist/types/tab-spacing.d.ts +0 -24
- package/dist/types/temp.d.ts +0 -17
- package/dist/types/template.d.ts +0 -62
- package/dist/types/timing-buffer.d.ts +0 -22
- package/dist/types/tls-fetch.d.ts +0 -37
- package/dist/types/turndown/gfm.d.ts +0 -11
- package/dist/types/turndown/html.d.ts +0 -5
- package/dist/types/turndown/service.d.ts +0 -21
- package/dist/types/turndown/types.d.ts +0 -70
- package/dist/types/turndown.d.ts +0 -4
- package/dist/types/type-guards.d.ts +0 -5
- package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +0 -41
- package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +0 -89
- package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +0 -7
- package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +0 -12
- package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +0 -66
- package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +0 -48
- package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +0 -43
- package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +0 -7
- package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +0 -56
- package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +0 -65
- package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +0 -27
- package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +0 -17
- package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +0 -7
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +0 -11
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +0 -34
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +0 -11
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +0 -11
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +0 -26
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +0 -31
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +0 -11
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +0 -59
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +0 -17
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +0 -30
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +0 -55
- package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +0 -206
- package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +0 -51
- package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +0 -2
- package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +0 -6
- package/dist/types/vendor/mermaid-ascii/class/types.d.ts +0 -102
- package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +0 -6
- package/dist/types/vendor/mermaid-ascii/er/types.d.ts +0 -76
- package/dist/types/vendor/mermaid-ascii/index.d.ts +0 -1
- package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +0 -9
- package/dist/types/vendor/mermaid-ascii/parser.d.ts +0 -7
- package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +0 -6
- package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +0 -130
- package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +0 -21
- package/dist/types/vendor/mermaid-ascii/types.d.ts +0 -114
- package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +0 -25
- package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +0 -6
- package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +0 -145
- package/dist/types/version.d.ts +0 -18
- package/dist/types/vterm/buffer.d.ts +0 -99
- package/dist/types/vterm/terminal.d.ts +0 -44
- package/dist/types/vterm.d.ts +0 -8
- package/dist/types/which.d.ts +0 -37
- package/dist/types/worker-host.d.ts +0 -51
- package/dist/types/xml.d.ts +0 -31
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [1.0.
|
|
5
|
+
## [1.0.2] - 2026-08-15
|
|
6
|
+
|
|
7
|
+
## [17.3.2] - 2026-08-13
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Fixed `fetchWithRetry()` aborts during retry backoff to preserve the documented `"Request was aborted"` error contract ([#8450](https://github.com/can1357/oh-my-pi/issues/8450)).
|
|
12
|
+
|
|
13
|
+
## [17.3.0] - 2026-08-13
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- Optimized performance of partial JSON parsing for long streaming tool-call arguments.
|
|
18
|
+
- Fixed Mermaid ASCII multi-word edge labels where routed lines would show through spaces.
|
|
19
|
+
|
|
20
|
+
## [17.2.15] - 2026-08-12
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Extended parsed Server-Sent Events (SSE) to include optional id and retry fields, enabling reconnecting transports to retain stream cursors and respect server-requested retry intervals.
|
|
25
|
+
|
|
26
|
+
## [17.2.13] - 2026-08-11
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Changed stale process-log retention from the newest five files globally to one newest file per completed process and day within the current and previous four local calendar days. This preserves bounded daily diagnostic coverage while continuing to remove one-use audit files.
|
|
31
|
+
- Changed outbound User-Agent consumers to share the versioned `USER_AGENT` constant (`omp/<version>`).
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Fixed Mermaid ASCII state pseudostates rendering empty boxes, miscoloring final-state borders, and inverting rounded corners in bottom-to-top diagrams.
|
|
6
36
|
|
|
7
37
|
## [17.2.11] - 2026-08-07
|
|
8
38
|
|
|
@@ -18,7 +48,7 @@
|
|
|
18
48
|
|
|
19
49
|
### Added
|
|
20
50
|
|
|
21
|
-
- Introduced zero-dependency in-house modules replacing external packages, importable via `@
|
|
51
|
+
- Introduced zero-dependency in-house modules replacing external packages, importable via `@linxiraos/pi-utils/<module>`: `acp` (Agent Client Protocol), `browsers` (Chrome for Testing discovery/install), `chalk` (ANSI styling), `dates` (date formatting), `dom` (HTML parser, WHATWG DOM subset, and CSS selectors), `docx` (DOCX to HTML), `headers` (browser header generation), `lru` (LRU cache), `marked` (GFM markdown lexer/parser), `readability` (article extraction), `template` (Handlebars-compatible templating), `turndown` (HTML to Markdown), `vterm` (headless terminal emulator), and `xml` (XML parser).
|
|
22
52
|
- Added postmortem fatal recovery hint providers to allow applications to print actionable recovery commands before cleanup starts.
|
|
23
53
|
|
|
24
54
|
### Changed
|
|
@@ -39,7 +69,7 @@
|
|
|
39
69
|
|
|
40
70
|
### Added
|
|
41
71
|
|
|
42
|
-
- Added a public `compareVersions` utility (`@
|
|
72
|
+
- Added a public `compareVersions` utility (`@linxiraos/pi-utils`) that compares two version strings with SemVer-2.0 prerelease ordering, build-metadata stripping, and numeric segment comparison without float overflow; never throws.
|
|
43
73
|
|
|
44
74
|
### Fixed
|
|
45
75
|
|
|
@@ -287,7 +317,7 @@
|
|
|
287
317
|
|
|
288
318
|
### Removed
|
|
289
319
|
|
|
290
|
-
- Removed the public `createAbortableStream` API from `@
|
|
320
|
+
- Removed the public `createAbortableStream` API from `@linxiraos/pi-utils`. Consumers should use the lighter, direct-reader `abortableSource` async generator inside `@linxiraos/pi-utils/stream` to avoid the extra ReadableStream wrapper layer and per-chunk enqueue overhead.
|
|
291
321
|
|
|
292
322
|
## [16.0.11] - 2026-06-19
|
|
293
323
|
|
|
@@ -316,7 +346,7 @@
|
|
|
316
346
|
|
|
317
347
|
### Added
|
|
318
348
|
|
|
319
|
-
- Added `installWorkerInbox(port)` / `consumeWorkerInbox()` to `@
|
|
349
|
+
- Added `installWorkerInbox(port)` / `consumeWorkerInbox()` to `@linxiraos/pi-utils/worker-host`. A self-dispatching CLI host that imports a Bun worker module dynamically attaches the worker's real `message` listener after Bun flushes the messages the parent posted before spawn, dropping a synchronously-posted `init`. The host installs this buffering inbox synchronously in the entry's sync prefix so a listener exists at flush time; the worker module consumes it and binds the real handler, replaying anything buffered.
|
|
320
350
|
|
|
321
351
|
## [15.13.1] - 2026-06-15
|
|
322
352
|
|
|
@@ -327,7 +357,7 @@
|
|
|
327
357
|
- Added support for a runtime `overrides` map in `RuntimeInstallSpec`, which is now written into generated runtime `package.json` manifests to force dependency pins (including transitive ones) across the runtime tree
|
|
328
358
|
- Added a lightweight loop-phase breadcrumb stack (`pushLoopPhase`/`popLoopPhase`/`currentLoopPhase`, plus `takeRecentLoopPhase` which returns the live phase or the most recently popped one and clears it) so the TUI event-loop watchdog can attribute a main-thread block to the phase that caused it — including a synchronous phase already popped before the watchdog's delayed tick runs ([#2485](https://github.com/can1357/oh-my-pi/issues/2485))
|
|
329
359
|
- Added `FetchWithRetryOptions.timeout` (forwarded to the underlying `fetch` call). `false` disables Bun's native ~300s pre-response timeout; a positive number overrides the ceiling. Bare browser/Node fetch ignores it ([#2422](https://github.com/can1357/oh-my-pi/issues/2422))
|
|
330
|
-
- Added the side-effect-free `@
|
|
360
|
+
- Added the side-effect-free `@linxiraos/pi-utils/worker-host` module (`declareWorkerHostEntry()` / `workerHostEntry()`), extracted from `env` (still re-exported there) so worker spawn sites can resolve the self-dispatching CLI host entry without importing `env`'s side-effecting module graph.
|
|
331
361
|
|
|
332
362
|
### Fixed
|
|
333
363
|
|
package/package.json
CHANGED
|
@@ -1,59 +1,58 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
2
|
+
"type": "module",
|
|
3
|
+
"name": "@linxiraos/pi-utils",
|
|
4
|
+
"version": "1.0.2",
|
|
5
|
+
"description": "Shared utilities for pi packages",
|
|
6
|
+
"homepage": "https://linxira-os.github.io",
|
|
7
|
+
"author": "Can Boluk",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Linxira-OS/linxira-zeta.git",
|
|
12
|
+
"directory": "packages/utils"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/can1357/oh-my-pi/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"utilities",
|
|
19
|
+
"cli",
|
|
20
|
+
"logging",
|
|
21
|
+
"streams"
|
|
22
|
+
],
|
|
23
|
+
"main": "./src/index.ts",
|
|
24
|
+
"types": "./src/index.ts",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"check": "biome check . && bun run check:types",
|
|
27
|
+
"check:types": "tsgo -p tsconfig.json --noEmit",
|
|
28
|
+
"lint": "biome lint .",
|
|
29
|
+
"test": "bun test --parallel",
|
|
30
|
+
"fix": "biome check --write --unsafe .",
|
|
31
|
+
"fmt": "biome format --write ."
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@linxiraos/pi-natives": "catalog:"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/bun": "catalog:"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"bun": ">=1.3.14"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"src",
|
|
44
|
+
"README.md",
|
|
45
|
+
"CHANGELOG.md"
|
|
46
|
+
],
|
|
47
|
+
"exports": {
|
|
48
|
+
".": {
|
|
49
|
+
"types": "./src/index.ts",
|
|
50
|
+
"import": "./src/index.ts"
|
|
51
|
+
},
|
|
52
|
+
"./*": {
|
|
53
|
+
"types": "./src/*.ts",
|
|
54
|
+
"import": "./src/*.ts"
|
|
55
|
+
},
|
|
56
|
+
"./*.js": "./src/*.ts"
|
|
57
|
+
}
|
|
59
58
|
}
|
package/src/dirs.ts
CHANGED
|
@@ -28,6 +28,9 @@ export const MAIN_CONFIG_FILENAMES = ["config.yml", "config.yaml"] as const;
|
|
|
28
28
|
/** Version (e.g. "1.0.0") */
|
|
29
29
|
export const VERSION: string = version;
|
|
30
30
|
|
|
31
|
+
/** Default User-Agent header string (e.g. "omp/17.2.12") */
|
|
32
|
+
export const USER_AGENT = `omp/${VERSION}`;
|
|
33
|
+
|
|
31
34
|
/** Minimum Bun version */
|
|
32
35
|
export const MIN_BUN_VERSION: string = engines.bun.replace(/[^0-9.]/g, "");
|
|
33
36
|
|
package/src/fetch-retry.ts
CHANGED
|
@@ -220,7 +220,7 @@ export async function fetchWithRetry(
|
|
|
220
220
|
if (signal?.aborted) throw new Error("Request was aborted");
|
|
221
221
|
const wrapped = wrapNetworkError(error);
|
|
222
222
|
if (attempt + 1 >= maxAttempts) throw wrapped;
|
|
223
|
-
await
|
|
223
|
+
await waitForRetry(resolveDefaultDelay(defaultDelayMs, attempt, maxDelayMs), signal);
|
|
224
224
|
continue;
|
|
225
225
|
}
|
|
226
226
|
|
|
@@ -234,7 +234,7 @@ export async function fetchWithRetry(
|
|
|
234
234
|
if (hint !== undefined && hint > maxDelayMs) return response;
|
|
235
235
|
|
|
236
236
|
const delayMs = Math.min(hint ?? resolveDefaultDelay(defaultDelayMs, attempt, maxDelayMs), maxDelayMs);
|
|
237
|
-
await
|
|
237
|
+
await waitForRetry(delayMs, signal);
|
|
238
238
|
}
|
|
239
239
|
}
|
|
240
240
|
|
|
@@ -251,6 +251,15 @@ function mergeInit(base: RequestInit, overlay: RequestInit, timeout: number | fa
|
|
|
251
251
|
return merged;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
async function waitForRetry(delayMs: number, signal: AbortSignal | undefined): Promise<void> {
|
|
255
|
+
try {
|
|
256
|
+
await scheduler.wait(delayMs, { signal });
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (signal?.aborted) throw new Error("Request was aborted");
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
254
263
|
function wrapNetworkError(error: unknown): Error {
|
|
255
264
|
if (error instanceof Error) {
|
|
256
265
|
if (error.name === "AbortError" || error.message === "Request was aborted") {
|
package/src/json-parse.ts
CHANGED
|
@@ -579,8 +579,8 @@ export function parseStreamingJson<T = Record<string, unknown>>(partialJson: str
|
|
|
579
579
|
|
|
580
580
|
/**
|
|
581
581
|
* Default minimum byte growth before `parseStreamingJsonThrottled` will
|
|
582
|
-
* re-parse a streaming tool-call argument buffer.
|
|
583
|
-
*
|
|
582
|
+
* re-parse a streaming tool-call argument buffer. Acts as the floor of the
|
|
583
|
+
* geometric gate — see {@link parseStreamingJsonThrottled}.
|
|
584
584
|
*/
|
|
585
585
|
export const STREAMING_JSON_PARSE_MIN_GROWTH = 256;
|
|
586
586
|
|
|
@@ -589,14 +589,23 @@ export const STREAMING_JSON_PARSE_MIN_GROWTH = 256;
|
|
|
589
589
|
*
|
|
590
590
|
* Tool calls arrive as a long sequence of small deltas — calling
|
|
591
591
|
* `parseStreamingJson(buffer)` on every delta re-parses the entire buffer
|
|
592
|
-
* each time, giving O(N²) work in the total buffer length.
|
|
593
|
-
*
|
|
594
|
-
*
|
|
592
|
+
* each time, giving O(N²) work in the total buffer length. A fixed re-parse
|
|
593
|
+
* floor alone does NOT fix this: with `minGrowthBytes` constant, a buffer of
|
|
594
|
+
* length N is parsed N/minGrowthBytes times at an average cost of N/2, which
|
|
595
|
+
* is still O(N²) (the constant just shrinks). Long `write` payloads — where
|
|
596
|
+
* the buffer is the whole file — made this the dominant main-thread stall
|
|
597
|
+
* during streaming.
|
|
598
|
+
*
|
|
599
|
+
* Instead the gate scales geometrically: once the buffer is large, a re-parse
|
|
600
|
+
* requires growth proportional to the current length (`len / 32`, floored at
|
|
601
|
+
* `minGrowthBytes`). Parse points then form a geometric progression, so a
|
|
602
|
+
* buffer of length N is parsed O(log N) times for O(N log N) total work,
|
|
603
|
+
* while small buffers keep the snappy fixed-cadence updates.
|
|
595
604
|
*
|
|
596
605
|
* Each provider tracks the last parsed length on its tool-call block, so the
|
|
597
606
|
* final `toolcall_end` parse (which providers already perform unconditionally)
|
|
598
607
|
* is the authoritative full parse — the throttle only delays mid-stream UI
|
|
599
|
-
* updates by at most
|
|
608
|
+
* updates, by at most ~3% of the accumulated content for large buffers.
|
|
600
609
|
*
|
|
601
610
|
* @returns the parsed object plus the new `parsedLen` to persist; or `null`
|
|
602
611
|
* when the buffer has not grown enough to warrant a re-parse.
|
|
@@ -607,7 +616,9 @@ export function parseStreamingJsonThrottled<T = Record<string, unknown>>(
|
|
|
607
616
|
minGrowthBytes: number = STREAMING_JSON_PARSE_MIN_GROWTH,
|
|
608
617
|
): { value: T; parsedLen: number } | null {
|
|
609
618
|
const len = partialJson?.length ?? 0;
|
|
610
|
-
if (len === 0
|
|
619
|
+
if (len === 0) return null;
|
|
620
|
+
const growth = Math.max(minGrowthBytes, len >> 5);
|
|
621
|
+
if (lastParsedLen > 0 && len - lastParsedLen < growth) return null;
|
|
611
622
|
return { value: parseStreamingJson<T>(partialJson), parsedLen: len };
|
|
612
623
|
}
|
|
613
624
|
|
package/src/logger.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (the auth broker, etc.) call {@link setTransports} to swap in a console
|
|
7
7
|
* transport so a process supervisor (pm2, journald, k8s) captures the logs.
|
|
8
8
|
*
|
|
9
|
-
* Each entry includes `process.pid` so concurrent
|
|
9
|
+
* Each entry includes `process.pid` so concurrent omp instances stay
|
|
10
10
|
* traceable.
|
|
11
11
|
*/
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -53,9 +53,11 @@ function emitToSinks(level: LogLevel, message: string, context: Record<string, u
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const PROCESS_LOG_PATTERN = /^zeta
|
|
56
|
+
const PROCESS_LOG_PATTERN = /^zeta\.(\d{4}-\d{2}-\d{2})\.(\d+)\.log(?:\.(\d+))?$/;
|
|
57
57
|
const PROCESS_AUDIT_PATTERN = /^\.zeta\.(\d+)-audit\.json$/;
|
|
58
|
-
const
|
|
58
|
+
const RETAINED_STALE_LOGS_PER_PROCESS_DAY = 1;
|
|
59
|
+
const RETAINED_STALE_AUDIT_FILES = 0;
|
|
60
|
+
const RETAINED_STALE_LOG_DAYS = 5;
|
|
59
61
|
|
|
60
62
|
function processIsRunning(pid: number): boolean {
|
|
61
63
|
try {
|
|
@@ -67,8 +69,10 @@ function processIsRunning(pid: number): boolean {
|
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
/**
|
|
70
|
-
* Retain
|
|
71
|
-
*
|
|
72
|
+
* Retain one newest completed-process log per process/day within the current
|
|
73
|
+
* and previous four local calendar days, and remove one-use audit files. Live
|
|
74
|
+
* PID namespaces are never touched. The calendar-day boundary preserves daily
|
|
75
|
+
* diagnostic coverage while bounding completed-process storage and scans.
|
|
72
76
|
*/
|
|
73
77
|
function pruneStaleProcessLogs(dir: string): void {
|
|
74
78
|
let entries: fs.Dirent[];
|
|
@@ -77,38 +81,69 @@ function pruneStaleProcessLogs(dir: string): void {
|
|
|
77
81
|
} catch {
|
|
78
82
|
return;
|
|
79
83
|
}
|
|
80
|
-
|
|
81
|
-
const
|
|
84
|
+
const current = new Date();
|
|
85
|
+
const currentDate =
|
|
86
|
+
`${current.getFullYear()}-${String(current.getMonth() + 1).padStart(2, "0")}-` +
|
|
87
|
+
String(current.getDate()).padStart(2, "0");
|
|
88
|
+
const cutoff = new Date(current);
|
|
89
|
+
cutoff.setDate(cutoff.getDate() - (RETAINED_STALE_LOG_DAYS - 1));
|
|
90
|
+
const cutoffDate =
|
|
91
|
+
`${cutoff.getFullYear()}-${String(cutoff.getMonth() + 1).padStart(2, "0")}-` +
|
|
92
|
+
String(cutoff.getDate()).padStart(2, "0");
|
|
93
|
+
|
|
94
|
+
const staleLogsByProcessDay = new Map<string, Array<{ path: string; mtimeMs: number; rollover: number }>>();
|
|
82
95
|
for (const entry of entries) {
|
|
83
96
|
if (!entry.isFile()) continue;
|
|
84
97
|
const logMatch = PROCESS_LOG_PATTERN.exec(entry.name);
|
|
85
98
|
const auditMatch = PROCESS_AUDIT_PATTERN.exec(entry.name);
|
|
86
|
-
const pidText = logMatch?.[
|
|
99
|
+
const pidText = logMatch?.[2] ?? auditMatch?.[1];
|
|
87
100
|
if (!pidText || processIsRunning(Number(pidText))) continue;
|
|
88
101
|
const entryPath = path.join(dir, entry.name);
|
|
89
102
|
|
|
90
103
|
if (auditMatch) {
|
|
104
|
+
if (RETAINED_STALE_AUDIT_FILES === 0) {
|
|
105
|
+
try {
|
|
106
|
+
fs.rmSync(entryPath, { force: true });
|
|
107
|
+
} catch {
|
|
108
|
+
// Retention is best-effort; logging must still initialize.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!logMatch?.[1]) continue;
|
|
114
|
+
if (logMatch[1] < cutoffDate || logMatch[1] > currentDate) {
|
|
91
115
|
try {
|
|
92
116
|
fs.rmSync(entryPath, { force: true });
|
|
93
117
|
} catch {
|
|
94
|
-
//
|
|
118
|
+
// Another process may have pruned the same stale namespace.
|
|
95
119
|
}
|
|
96
120
|
continue;
|
|
97
121
|
}
|
|
98
122
|
|
|
99
123
|
try {
|
|
100
|
-
|
|
124
|
+
const key = `${pidText}:${logMatch[1]}`;
|
|
125
|
+
const staleLogs = staleLogsByProcessDay.get(key) ?? [];
|
|
126
|
+
staleLogs.push({
|
|
127
|
+
path: entryPath,
|
|
128
|
+
mtimeMs: fs.statSync(entryPath).mtimeMs,
|
|
129
|
+
rollover: Number(logMatch[3] ?? 0),
|
|
130
|
+
});
|
|
131
|
+
staleLogsByProcessDay.set(key, staleLogs);
|
|
101
132
|
} catch {
|
|
102
133
|
// Another process may have pruned the same stale namespace.
|
|
103
134
|
}
|
|
104
135
|
}
|
|
105
136
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
137
|
+
for (const staleLogs of staleLogsByProcessDay.values()) {
|
|
138
|
+
staleLogs.sort(
|
|
139
|
+
(a, b) => b.mtimeMs - a.mtimeMs || b.rollover - a.rollover || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0),
|
|
140
|
+
);
|
|
141
|
+
for (const stale of staleLogs.slice(RETAINED_STALE_LOGS_PER_PROCESS_DAY)) {
|
|
142
|
+
try {
|
|
143
|
+
fs.rmSync(stale.path, { force: true });
|
|
144
|
+
} catch {
|
|
145
|
+
// Another process may have pruned the same stale namespace.
|
|
146
|
+
}
|
|
112
147
|
}
|
|
113
148
|
}
|
|
114
149
|
}
|
package/src/ptree.ts
CHANGED
|
@@ -217,11 +217,11 @@ export class ChildProcess<In extends InMask = InMask> {
|
|
|
217
217
|
return this;
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
kill(reason?: Exception) {
|
|
220
|
+
kill(reason?: Exception, gracefulMs?: number) {
|
|
221
221
|
if (reason && !this.#exitReasonPending) this.#exitReasonPending = reason;
|
|
222
222
|
if (!this.proc.killed)
|
|
223
223
|
void Process.fromPid(this.proc.pid)
|
|
224
|
-
?.terminate()
|
|
224
|
+
?.terminate(gracefulMs === undefined ? undefined : { gracefulMs })
|
|
225
225
|
?.catch(e => void e);
|
|
226
226
|
}
|
|
227
227
|
|
package/src/stream.ts
CHANGED
|
@@ -281,11 +281,16 @@ export async function* readSseJson<T>(
|
|
|
281
281
|
* - `raw` is the list of decoded non-empty lines that made up the event,
|
|
282
282
|
* preserved for diagnostic context (error reporting, debugging). The
|
|
283
283
|
* dispatching blank line is not included.
|
|
284
|
+
* - `id` and `retry` are present only when the event carried valid fields with
|
|
285
|
+
* those names. Control-only events are yielded so reconnecting transports can
|
|
286
|
+
* retain the cursor and server-requested retry interval.
|
|
284
287
|
*/
|
|
285
288
|
export interface ServerSentEvent {
|
|
286
289
|
event: string | null;
|
|
287
290
|
data: string;
|
|
288
291
|
raw: string[];
|
|
292
|
+
id?: string;
|
|
293
|
+
retry?: number;
|
|
289
294
|
}
|
|
290
295
|
|
|
291
296
|
interface SseEventState {
|
|
@@ -296,6 +301,8 @@ interface SseEventState {
|
|
|
296
301
|
// seen yet" (distinct from a `data:` field with an empty value).
|
|
297
302
|
data: string | null;
|
|
298
303
|
raw: string[];
|
|
304
|
+
id?: string;
|
|
305
|
+
retry?: number;
|
|
299
306
|
}
|
|
300
307
|
|
|
301
308
|
// Complete lines are decoded in one batch per source chunk. Each batch ends on
|
|
@@ -303,7 +310,7 @@ interface SseEventState {
|
|
|
303
310
|
const SSE_DECODER = new TextDecoder("utf-8");
|
|
304
311
|
|
|
305
312
|
function flushSseEvent(state: SseEventState): ServerSentEvent | null {
|
|
306
|
-
if (state.event === null && state.data === null) {
|
|
313
|
+
if (state.event === null && state.data === null && state.id === undefined && state.retry === undefined) {
|
|
307
314
|
state.raw = [];
|
|
308
315
|
return null;
|
|
309
316
|
}
|
|
@@ -312,9 +319,13 @@ function flushSseEvent(state: SseEventState): ServerSentEvent | null {
|
|
|
312
319
|
data: state.data ?? "",
|
|
313
320
|
raw: state.raw,
|
|
314
321
|
};
|
|
322
|
+
if (state.id !== undefined) event.id = state.id;
|
|
323
|
+
if (state.retry !== undefined) event.retry = state.retry;
|
|
315
324
|
state.event = null;
|
|
316
325
|
state.data = null;
|
|
317
326
|
state.raw = [];
|
|
327
|
+
state.id = undefined;
|
|
328
|
+
state.retry = undefined;
|
|
318
329
|
return event;
|
|
319
330
|
}
|
|
320
331
|
|
|
@@ -348,9 +359,22 @@ function pushSseLine(line: string, state: SseEventState): ServerSentEvent | null
|
|
|
348
359
|
state.data += "\n";
|
|
349
360
|
state.data += value;
|
|
350
361
|
}
|
|
362
|
+
} else if (fieldName === "id") {
|
|
363
|
+
if (!value.includes("\0")) state.id = value;
|
|
364
|
+
} else if (fieldName === "retry" && value.length > 0) {
|
|
365
|
+
let valid = true;
|
|
366
|
+
for (let index = 0; index < value.length; index++) {
|
|
367
|
+
const code = value.charCodeAt(index);
|
|
368
|
+
if (code < 0x30 || code > 0x39) {
|
|
369
|
+
valid = false;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (valid) {
|
|
374
|
+
const retry = Number(value);
|
|
375
|
+
if (Number.isSafeInteger(retry)) state.retry = retry;
|
|
376
|
+
}
|
|
351
377
|
}
|
|
352
|
-
// `id` and `retry` are intentionally ignored — the providers we consume
|
|
353
|
-
// don't use them, and the underlying transport handles reconnects itself.
|
|
354
378
|
return null;
|
|
355
379
|
}
|
|
356
380
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import type { Canvas, DrawingCoord, RoleCanvas, CharRole, AsciiTheme, ColorMode } from './types'
|
|
10
10
|
import { colorizeLine, DEFAULT_ASCII_THEME } from './ansi'
|
|
11
|
-
import { displayWidth, toCells, WIDE_PAD } from '../text-metrics'
|
|
11
|
+
import { displayWidth, LABEL_SPACE, toCells, WIDE_PAD } from '../text-metrics'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Create a blank canvas filled with spaces.
|
|
@@ -189,7 +189,7 @@ export function isJunctionChar(c: string): boolean {
|
|
|
189
189
|
* letter/digit test misses.
|
|
190
190
|
*/
|
|
191
191
|
function isLabelChar(c: string): boolean {
|
|
192
|
-
return c === WIDE_PAD || displayWidth(c) === 2 || /[\p{L}\p{N}]/u.test(c)
|
|
192
|
+
return c === LABEL_SPACE || c === WIDE_PAD || displayWidth(c) === 2 || /[\p{L}\p{N}]/u.test(c)
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
/**
|
|
@@ -268,7 +268,7 @@ export function mergeCanvases(
|
|
|
268
268
|
for (let x = 0; x < overlay.length; x++) {
|
|
269
269
|
for (let y = 0; y < overlay[0]!.length; y++) {
|
|
270
270
|
const c = overlay[x]![y]!
|
|
271
|
-
// WIDE_PAD cells are written atomically with their lead below
|
|
271
|
+
// Spaces are transparent; WIDE_PAD cells are written atomically with their lead below
|
|
272
272
|
if (c === ' ' || c === WIDE_PAD) continue
|
|
273
273
|
const mx = x + offset.x
|
|
274
274
|
const my = y + offset.y
|
|
@@ -327,8 +327,8 @@ export function canvasToString(canvas: Canvas, options?: CanvasToStringOptions):
|
|
|
327
327
|
let line = ''
|
|
328
328
|
for (let x = 0; x <= maxX; x++) {
|
|
329
329
|
const c = canvas[x]![y]!
|
|
330
|
-
// Skip wide-glyph continuation cells
|
|
331
|
-
if (c !== WIDE_PAD) line += c
|
|
330
|
+
// Skip wide-glyph continuation cells and restore opaque label spaces.
|
|
331
|
+
if (c !== WIDE_PAD) line += c === LABEL_SPACE ? ' ' : c
|
|
332
332
|
}
|
|
333
333
|
lines.push(line)
|
|
334
334
|
} else {
|
|
@@ -338,7 +338,7 @@ export function canvasToString(canvas: Canvas, options?: CanvasToStringOptions):
|
|
|
338
338
|
for (let x = 0; x <= maxX; x++) {
|
|
339
339
|
const c = canvas[x]![y]!
|
|
340
340
|
if (c === WIDE_PAD) continue
|
|
341
|
-
chars.push(c)
|
|
341
|
+
chars.push(c === LABEL_SPACE ? ' ' : c)
|
|
342
342
|
roles.push(roleCanvas[x]?.[y] ?? null)
|
|
343
343
|
}
|
|
344
344
|
lines.push(colorizeLine(chars, roles, theme, colorMode))
|
|
@@ -370,6 +370,8 @@ const VERTICAL_FLIP_MAP: Record<string, string> = {
|
|
|
370
370
|
// Unicode corners
|
|
371
371
|
'┌': '└', '└': '┌',
|
|
372
372
|
'┐': '┘', '┘': '┐',
|
|
373
|
+
'╭': '╰', '╰': '╭',
|
|
374
|
+
'╮': '╯', '╯': '╮',
|
|
373
375
|
// Unicode junctions (T-pieces flip vertically)
|
|
374
376
|
'┬': '┴', '┴': '┬',
|
|
375
377
|
// Box-start junctions (exit points from node boxes)
|
|
@@ -21,7 +21,7 @@ import { gridToDrawingCoord, lineToDrawing } from './grid'
|
|
|
21
21
|
import { splitLines } from './multiline-utils'
|
|
22
22
|
import { getCorners } from './shapes/corners'
|
|
23
23
|
import { getShapeAttachmentPoint } from './shapes/index'
|
|
24
|
-
import { displayWidth, toCells, WIDE_PAD } from '../text-metrics'
|
|
24
|
+
import { displayWidth, LABEL_SPACE, toCells, WIDE_PAD } from '../text-metrics'
|
|
25
25
|
|
|
26
26
|
// ============================================================================
|
|
27
27
|
// Node drawing — renders a node using shape-aware rendering
|
|
@@ -76,16 +76,18 @@ function drawBoxWithGridDimensions(node: AsciiNode, graph: AsciiGraph): Canvas {
|
|
|
76
76
|
// Get corner characters for this shape type
|
|
77
77
|
const corners = getCorners(node.shape, useAscii)
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
const hChar = useAscii ? (
|
|
82
|
-
const vChar = useAscii ? (
|
|
79
|
+
const isStateStart = node.shape === 'state-start'
|
|
80
|
+
const isStateEnd = node.shape === 'state-end'
|
|
81
|
+
const hChar = useAscii ? (isStateEnd ? '=' : '-') : (isStateEnd ? '═' : '─')
|
|
82
|
+
const vChar = useAscii ? (isStateEnd ? '‖' : '|') : (isStateEnd ? '║' : '│')
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
const stateStartCorners = useAscii
|
|
85
|
+
? { tl: '+', tr: '+', bl: '+', br: '+' }
|
|
86
|
+
: { tl: '╭', tr: '╮', bl: '╰', br: '╯' }
|
|
87
|
+
const stateEndCorners = useAscii
|
|
86
88
|
? { tl: '#', tr: '#', bl: '#', br: '#' }
|
|
87
89
|
: { tl: '╔', tr: '╗', bl: '╚', br: '╝' }
|
|
88
|
-
const effectiveCorners =
|
|
90
|
+
const effectiveCorners = isStateEnd ? stateEndCorners : isStateStart ? stateStartCorners : corners
|
|
89
91
|
|
|
90
92
|
// Draw box border with shape-specific corners
|
|
91
93
|
for (let x = from.x + 1; x < to.x; x++) box[x]![from.y] = hChar
|
|
@@ -97,8 +99,8 @@ function drawBoxWithGridDimensions(node: AsciiNode, graph: AsciiGraph): Canvas {
|
|
|
97
99
|
box[from.x]![to.y] = effectiveCorners.bl
|
|
98
100
|
box[to.x]![to.y] = effectiveCorners.br
|
|
99
101
|
|
|
100
|
-
//
|
|
101
|
-
const label = node.displayLabel
|
|
102
|
+
// Pseudostates have no source label; restore their UML marker explicitly.
|
|
103
|
+
const label = node.displayLabel || (isStateStart ? (useAscii ? '*' : '●') : isStateEnd ? (useAscii ? '*' : '◎') : '')
|
|
102
104
|
const lines = splitLines(label)
|
|
103
105
|
const textCenterY = from.y + Math.floor(h / 2)
|
|
104
106
|
const startY = textCenterY - Math.floor((lines.length - 1) / 2)
|
|
@@ -677,7 +679,7 @@ function drawTextOnLine(canvas: Canvas, line: DrawingCoord[], label: string, isU
|
|
|
677
679
|
for (let i = 0; i < lines.length; i++) {
|
|
678
680
|
const lineText = lines[i]!
|
|
679
681
|
const startX = middleX - Math.floor(displayWidth(lineText) / 2)
|
|
680
|
-
drawText(canvas, { x: startX, y: startY + i }, lineText)
|
|
682
|
+
drawText(canvas, { x: startX, y: startY + i }, lineText.replaceAll(' ', LABEL_SPACE))
|
|
681
683
|
}
|
|
682
684
|
}
|
|
683
685
|
|
|
@@ -1223,7 +1225,8 @@ function fillRolesFromCanvases(
|
|
|
1223
1225
|
|
|
1224
1226
|
/**
|
|
1225
1227
|
* Special handling for node boxes: border chars get 'border' role, text gets 'text' role.
|
|
1226
|
-
*
|
|
1228
|
+
* Common final-state border characters (`#` and `=`) count only on the outer edge,
|
|
1229
|
+
* so identical characters inside ordinary node labels retain the text role.
|
|
1227
1230
|
*/
|
|
1228
1231
|
function fillRolesForNodeBox(
|
|
1229
1232
|
roleCanvas: RoleCanvas,
|
|
@@ -1231,7 +1234,9 @@ function fillRolesForNodeBox(
|
|
|
1231
1234
|
offset: DrawingCoord,
|
|
1232
1235
|
): void {
|
|
1233
1236
|
const isBorderChar = (c: string) => /^[┌┐└┘├┤┬┴┼│─╭╮╰╯+\-|.':]$/.test(c)
|
|
1234
|
-
|
|
1237
|
+
const isStateEndBorderChar = (c: string) => /^[╔╗╚╝═║#=‖]$/.test(c)
|
|
1238
|
+
const maxX = canvas.length - 1
|
|
1239
|
+
const maxY = (canvas[0]?.length ?? 1) - 1
|
|
1235
1240
|
for (let x = 0; x < canvas.length; x++) {
|
|
1236
1241
|
for (let y = 0; y < (canvas[0]?.length ?? 0); y++) {
|
|
1237
1242
|
const char = canvas[x]?.[y]
|
|
@@ -1240,7 +1245,9 @@ function fillRolesForNodeBox(
|
|
|
1240
1245
|
const ry = y + offset.y
|
|
1241
1246
|
// Use setRole which auto-expands the role canvas if needed
|
|
1242
1247
|
if (rx >= 0 && ry >= 0) {
|
|
1243
|
-
|
|
1248
|
+
const isOuterEdge = x === 0 || x === maxX || y === 0 || y === maxY
|
|
1249
|
+
const role = isBorderChar(char) || (isOuterEdge && isStateEndBorderChar(char)) ? 'border' : 'text'
|
|
1250
|
+
setRole(roleCanvas, rx, ry, role)
|
|
1244
1251
|
}
|
|
1245
1252
|
}
|
|
1246
1253
|
}
|