@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.
Files changed (131) hide show
  1. package/CHANGELOG.md +36 -6
  2. package/package.json +56 -57
  3. package/src/dirs.ts +3 -0
  4. package/src/fetch-retry.ts +11 -2
  5. package/src/json-parse.ts +18 -7
  6. package/src/logger.ts +51 -16
  7. package/src/ptree.ts +2 -2
  8. package/src/stream.ts +27 -3
  9. package/src/vendor/mermaid-ascii/ascii/canvas.ts +8 -6
  10. package/src/vendor/mermaid-ascii/ascii/draw.ts +21 -14
  11. package/src/vendor/mermaid-ascii/text-metrics.ts +3 -0
  12. package/dist/types/abortable.d.ts +0 -32
  13. package/dist/types/acp/connection.d.ts +0 -118
  14. package/dist/types/acp/protocol.d.ts +0 -526
  15. package/dist/types/acp/schema.d.ts +0 -41
  16. package/dist/types/acp/stream.d.ts +0 -8
  17. package/dist/types/acp/transport.d.ts +0 -81
  18. package/dist/types/acp.d.ts +0 -6
  19. package/dist/types/async.d.ts +0 -20
  20. package/dist/types/binary.d.ts +0 -17
  21. package/dist/types/browsers.d.ts +0 -68
  22. package/dist/types/chalk.d.ts +0 -125
  23. package/dist/types/cli.d.ts +0 -134
  24. package/dist/types/color.d.ts +0 -102
  25. package/dist/types/dates.d.ts +0 -7
  26. package/dist/types/dirs.d.ts +0 -290
  27. package/dist/types/docx/converter.d.ts +0 -46
  28. package/dist/types/docx/xml.d.ts +0 -26
  29. package/dist/types/docx/zip.d.ts +0 -6
  30. package/dist/types/docx.d.ts +0 -11
  31. package/dist/types/dom/core.d.ts +0 -431
  32. package/dist/types/dom/parser.d.ts +0 -7
  33. package/dist/types/dom/selector.d.ts +0 -5
  34. package/dist/types/dom.d.ts +0 -5
  35. package/dist/types/env.d.ts +0 -124
  36. package/dist/types/fetch-retry.d.ts +0 -96
  37. package/dist/types/file-lock.d.ts +0 -23
  38. package/dist/types/format.d.ts +0 -37
  39. package/dist/types/frontmatter.d.ts +0 -46
  40. package/dist/types/fs-error.d.ts +0 -31
  41. package/dist/types/glob.d.ts +0 -28
  42. package/dist/types/headers.d.ts +0 -34
  43. package/dist/types/index.d.ts +0 -39
  44. package/dist/types/json-parse.d.ts +0 -74
  45. package/dist/types/json.d.ts +0 -14
  46. package/dist/types/logger/rotating-file.d.ts +0 -18
  47. package/dist/types/logger.d.ts +0 -96
  48. package/dist/types/loop-phase.d.ts +0 -10
  49. package/dist/types/lru.d.ts +0 -46
  50. package/dist/types/marked/core.d.ts +0 -445
  51. package/dist/types/marked.d.ts +0 -2
  52. package/dist/types/mermaid-ascii.d.ts +0 -11
  53. package/dist/types/mime.d.ts +0 -29
  54. package/dist/types/module-timer.d.ts +0 -1
  55. package/dist/types/path-tree.d.ts +0 -76
  56. package/dist/types/path.d.ts +0 -2
  57. package/dist/types/peek-file.d.ts +0 -29
  58. package/dist/types/postmortem.d.ts +0 -104
  59. package/dist/types/process-name.d.ts +0 -7
  60. package/dist/types/procmgr.d.ts +0 -74
  61. package/dist/types/prompt.d.ts +0 -18
  62. package/dist/types/ptree.d.ts +0 -110
  63. package/dist/types/readability/readability.d.ts +0 -9
  64. package/dist/types/readability/readerable.d.ts +0 -10
  65. package/dist/types/readability/types.d.ts +0 -70
  66. package/dist/types/readability.d.ts +0 -4
  67. package/dist/types/ring.d.ts +0 -93
  68. package/dist/types/runtime-install.d.ts +0 -78
  69. package/dist/types/sanitize-text.d.ts +0 -29
  70. package/dist/types/snowflake.d.ts +0 -25
  71. package/dist/types/stderr-guard.d.ts +0 -22
  72. package/dist/types/stream.d.ts +0 -67
  73. package/dist/types/tab-spacing.d.ts +0 -24
  74. package/dist/types/temp.d.ts +0 -17
  75. package/dist/types/template.d.ts +0 -62
  76. package/dist/types/timing-buffer.d.ts +0 -22
  77. package/dist/types/tls-fetch.d.ts +0 -37
  78. package/dist/types/turndown/gfm.d.ts +0 -11
  79. package/dist/types/turndown/html.d.ts +0 -5
  80. package/dist/types/turndown/service.d.ts +0 -21
  81. package/dist/types/turndown/types.d.ts +0 -70
  82. package/dist/types/turndown.d.ts +0 -4
  83. package/dist/types/type-guards.d.ts +0 -5
  84. package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +0 -41
  85. package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +0 -89
  86. package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +0 -7
  87. package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +0 -12
  88. package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +0 -66
  89. package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +0 -48
  90. package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +0 -43
  91. package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +0 -7
  92. package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +0 -56
  93. package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +0 -65
  94. package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +0 -27
  95. package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +0 -17
  96. package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +0 -7
  97. package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +0 -11
  98. package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +0 -34
  99. package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +0 -11
  100. package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +0 -11
  101. package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +0 -26
  102. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +0 -31
  103. package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +0 -11
  104. package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +0 -59
  105. package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +0 -17
  106. package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +0 -30
  107. package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +0 -55
  108. package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +0 -206
  109. package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +0 -51
  110. package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +0 -2
  111. package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +0 -6
  112. package/dist/types/vendor/mermaid-ascii/class/types.d.ts +0 -102
  113. package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +0 -6
  114. package/dist/types/vendor/mermaid-ascii/er/types.d.ts +0 -76
  115. package/dist/types/vendor/mermaid-ascii/index.d.ts +0 -1
  116. package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +0 -9
  117. package/dist/types/vendor/mermaid-ascii/parser.d.ts +0 -7
  118. package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +0 -6
  119. package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +0 -130
  120. package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +0 -21
  121. package/dist/types/vendor/mermaid-ascii/types.d.ts +0 -114
  122. package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +0 -25
  123. package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +0 -6
  124. package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +0 -145
  125. package/dist/types/version.d.ts +0 -18
  126. package/dist/types/vterm/buffer.d.ts +0 -99
  127. package/dist/types/vterm/terminal.d.ts +0 -44
  128. package/dist/types/vterm.d.ts +0 -8
  129. package/dist/types/which.d.ts +0 -37
  130. package/dist/types/worker-host.d.ts +0 -51
  131. 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.1] - 2026-08-14
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 `@oh-my-pi/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).
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 (`@oh-my-pi/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.
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 `@oh-my-pi/pi-utils`. Consumers should use the lighter, direct-reader `abortableSource` async generator inside `@oh-my-pi/pi-utils/stream` to avoid the extra ReadableStream wrapper layer and per-chunk enqueue overhead.
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 `@oh-my-pi/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.
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 `@oh-my-pi/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.
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
- "type": "module",
3
- "name": "@linxiraos/pi-utils",
4
- "version": "1.0.1",
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": "./dist/types/index.d.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": "1.0.1"
35
- },
36
- "devDependencies": {
37
- "@types/bun": "^1.3.14"
38
- },
39
- "engines": {
40
- "bun": ">=1.3.14"
41
- },
42
- "files": [
43
- "src",
44
- "README.md",
45
- "CHANGELOG.md",
46
- "dist/types"
47
- ],
48
- "exports": {
49
- ".": {
50
- "types": "./dist/types/index.d.ts",
51
- "import": "./src/index.ts"
52
- },
53
- "./*": {
54
- "types": "./dist/types/*.d.ts",
55
- "import": "./src/*.ts"
56
- },
57
- "./*.js": "./src/*.ts"
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
 
@@ -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 scheduler.wait(resolveDefaultDelay(defaultDelayMs, attempt, maxDelayMs), { signal });
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 scheduler.wait(delayMs, { signal });
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. Bounds the mid-stream
583
- * partial-parse cost from quadratic to linear in N.
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. Throttling skips
593
- * the re-parse until at least `minGrowthBytes` of new content has arrived
594
- * since the last successful parse, bounding mid-stream cost to O(N).
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 `minGrowthBytes` of accumulated partial content.
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 || (lastParsedLen > 0 && len - lastParsedLen < minGrowthBytes)) return null;
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 zeta instances stay
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\.\d{4}-\d{2}-\d{2}\.(\d+)\.log(?:\.\d+)?$/;
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 RETAINED_STALE_LOG_FILES = 5;
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 the newest completed-process logs globally and remove their one-use
71
- * audit files. Live PID namespaces are never touched.
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 staleLogs: Array<{ path: string; mtimeMs: number }> = [];
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?.[1] ?? auditMatch?.[1];
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
- // Retention is best-effort; logging must still initialize.
118
+ // Another process may have pruned the same stale namespace.
95
119
  }
96
120
  continue;
97
121
  }
98
122
 
99
123
  try {
100
- staleLogs.push({ path: entryPath, mtimeMs: fs.statSync(entryPath).mtimeMs });
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
- staleLogs.sort((a, b) => b.mtimeMs - a.mtimeMs);
107
- for (const stale of staleLogs.slice(RETAINED_STALE_LOG_FILES)) {
108
- try {
109
- fs.rmSync(stale.path, { force: true });
110
- } catch {
111
- // Another process may have pruned the same stale namespace.
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: the glyph itself spans 2 columns
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
- // State-end uses double border to differentiate from state-start
80
- const isDoubleBox = node.shape === 'state-end'
81
- const hChar = useAscii ? (isDoubleBox ? '=' : '-') : (isDoubleBox ? '═' : '─')
82
- const vChar = useAscii ? (isDoubleBox ? '‖' : '|') : (isDoubleBox ? '║' : '│')
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
- // Double-box corners (for state-end)
85
- const doubleCorners = useAscii
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 = isDoubleBox ? doubleCorners : corners
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
- // Center the multi-line display label inside the box
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
- * Detects text by checking if character is alphanumeric or common punctuation.
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
- setRole(roleCanvas, rx, ry, isBorderChar(char) ? 'border' : 'text')
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
  }
@@ -29,6 +29,9 @@
29
29
  */
30
30
  export const WIDE_PAD = '\u0000'
31
31
 
32
+ /** Opaque label-space placeholder that serializes back to a regular space. */
33
+ export const LABEL_SPACE = '\u0001'
34
+
32
35
  const graphemeSegmenter = new Intl.Segmenter()
33
36
 
34
37
  /**