@sebastianandreasson/pi-autonomous-agents 0.9.0 → 0.10.0

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/README.md CHANGED
@@ -89,6 +89,7 @@ pi-harness report
89
89
  pi-harness clear-history
90
90
  pi-harness visual-once
91
91
  pi-harness visualize
92
+ pi-harness debug-live
92
93
  pi-harness visual-review-worker
93
94
  ```
94
95
 
@@ -319,4 +320,26 @@ npm run check
319
320
  npm test
320
321
  ```
321
322
 
323
+ For local visualizer iteration against fake live SDK agent:
324
+
325
+ ```bash
326
+ npm run debug:live-ui
327
+ ```
328
+
329
+ For React/Vite visualizer UI dev loop:
330
+
331
+ ```bash
332
+ npm run dev:visualizer:ui
333
+ ```
334
+
335
+ For production visualizer UI build:
336
+
337
+ ```bash
338
+ npm run build:visualizer:ui
339
+ ```
340
+
341
+ This seeds `.pi-debug/live-ui/`, runs harness there with streaming fake SDK fixture, hosts visualizer, and gives stable local repro loop for UI work. React app lives in `visualizer-ui/`. Visualizer server now serves built assets from `visualizer-ui/dist/` and falls back to build-instructions page if build artifacts are missing.
342
+
343
+ See `docs/VISUALIZER_UI_PLAN.md` for migration plan.
344
+
322
345
  The package requires Node `>=20`.
@@ -35,8 +35,10 @@ Main package files:
35
35
  - `src/pi-prompts.mjs`: default prompt builders
36
36
  - `src/pi-visual-review.mjs`: multimodal visual-review worker
37
37
  - `src/pi-visual-once.mjs`: one-shot manual visual review runner
38
- - `src/pi-visualizer.mjs`: local web UI for orchestration flow and active stage
38
+ - `src/pi-visualizer.mjs`: local web UI entrypoint
39
+ - `src/pi-visualizer-server.mjs`: shared visualizer server/runtime
39
40
  - `src/pi-visualizer-shared.mjs`: flow-state helpers for visualizer
41
+ - `src/pi-debug-live.mjs`: local fake-live sandbox runner for visualizer debugging
40
42
  - `src/pi-report.mjs`: telemetry summary report
41
43
  - `templates/DEVELOPER.md`: default developer-role instructions template
42
44
  - `templates/TESTER.md`: default tester-role instructions template
@@ -49,6 +51,7 @@ pi-harness run
49
51
  pi-harness report
50
52
  pi-harness visual-once
51
53
  pi-harness visualize
54
+ pi-harness debug-live
52
55
  ```
53
56
 
54
57
  The package reads `PI_CONFIG_FILE` if provided. Otherwise it falls back to the bundled generic `pi.config.json`.
@@ -59,6 +62,8 @@ The package reads `PI_CONFIG_FILE` if provided. Otherwise it falls back to the b
59
62
 
60
63
  Visualizer reads active-run lock, TODO file, per-run state, per-run iteration summary, per-run last output snapshot, live feed JSONL, and telemetry to show current stage plus historical runs.
61
64
 
65
+ For local UI iteration in this package repo, use `pi-harness debug-live` to run against seeded fake live SDK sandbox.
66
+
62
67
  ## Config Contract
63
68
 
64
69
  Projects typically provide their own `pi.config.json` with fields such as:
@@ -0,0 +1,117 @@
1
+ # Visualizer UI migration plan
2
+
3
+ ## Goal
4
+
5
+ Replace inline browser JS/HTML in `src/pi-visualizer-server.mjs` with maintainable React frontend, while keeping Node visualizer server as API + SSE + static asset host.
6
+
7
+ ## Chosen stack
8
+
9
+ - React
10
+ - Vite
11
+ - TypeScript
12
+ - Zustand
13
+ - Plain CSS
14
+
15
+ ## Repo layout
16
+
17
+ ```text
18
+ src/
19
+ pi-visualizer-server.mjs # API, SSE, static asset host
20
+ visualizer-ui/
21
+ package.json
22
+ tsconfig.json
23
+ vite.config.ts
24
+ index.html
25
+ src/
26
+ main.tsx
27
+ App.tsx
28
+ api.ts
29
+ store.ts
30
+ types.ts
31
+ styles.css
32
+ components/
33
+ TodoList.tsx
34
+ FlowStrip.tsx
35
+ LiveFeed.tsx
36
+ CurrentEdits.tsx
37
+ DiagnosticsPanel.tsx
38
+ ```
39
+
40
+ ## State model
41
+
42
+ Zustand store owns:
43
+
44
+ - latest snapshot
45
+ - selected run
46
+ - selected todo
47
+ - selected event
48
+ - feed toggles
49
+ - SSE lifecycle
50
+ - initial load status / error
51
+
52
+ ## API contract
53
+
54
+ Current server routes:
55
+
56
+ - `GET /api/state` → full snapshot
57
+ - `GET /api/stream` → SSE full snapshots
58
+
59
+ Short term: React app consumes current full-snapshot SSE.
60
+
61
+ Next improvement:
62
+
63
+ - add monotonic `seq` to live feed entries
64
+ - optionally move SSE from full snapshots to patch events
65
+ - add snapshot version to ignore stale payloads
66
+
67
+ ## Migration phases
68
+
69
+ ### Phase 1
70
+ - scaffold `visualizer-ui/`
71
+ - keep current inline HTML as fallback
72
+ - add built asset serving from `visualizer-ui/dist`
73
+ - add `/api/state` alias
74
+
75
+ ### Phase 2
76
+ - port current layout into React components
77
+ - use Zustand store + initial snapshot fetch
78
+ - use SSE reconnect from frontend
79
+
80
+ ### Phase 3
81
+ - move feed/timeline ordering to stable `seq`
82
+ - reduce full rerenders
83
+ - preserve scroll behavior inside components
84
+
85
+ ### Phase 4
86
+ - remove inline browser app from server once built UI covers current features
87
+ - keep server only as API/static host
88
+
89
+ ## Dev workflow
90
+
91
+ Backend + fake live harness:
92
+
93
+ ```bash
94
+ npm run debug:live-ui
95
+ ```
96
+
97
+ Frontend dev server:
98
+
99
+ ```bash
100
+ npm run dev:visualizer:ui
101
+ ```
102
+
103
+ Frontend build:
104
+
105
+ ```bash
106
+ npm run build:visualizer:ui
107
+ ```
108
+
109
+ ## Notes
110
+
111
+ Current state:
112
+
113
+ - React/Vite/Zustand UI scaffold added
114
+ - built assets generated under `visualizer-ui/dist/`
115
+ - server serves built UI directly
116
+ - legacy inline browser app removed
117
+ - fallback page only shows build instructions when dist missing
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sebastianandreasson/pi-autonomous-agents",
3
3
  "private": false,
4
- "version": "0.9.0",
4
+ "version": "0.10.0",
5
5
  "type": "module",
6
6
  "description": "Portable unattended PI harness for developer/tester/visual-review loops.",
7
7
  "license": "MIT",
@@ -19,13 +19,17 @@
19
19
  "@mariozechner/pi-coding-agent": "^0.66.1"
20
20
  },
21
21
  "scripts": {
22
- "check": "node --check src/cli.mjs && node --check src/pi-clear-history.mjs && node --check src/pi-client.mjs && node --check src/pi-config.mjs && node --check src/pi-flow.mjs && node --check src/pi-heartbeat.mjs && node --check src/pi-history.mjs && node --check src/pi-preflight.mjs && node --check src/pi-prompts.mjs && node --check src/pi-repo.mjs && node --check src/pi-report.mjs && node --check src/pi-sdk-turn.mjs && node --check src/pi-supervisor.mjs && node --check src/pi-telemetry.mjs && node --check src/pi-visual-once.mjs && node --check src/pi-visual-review.mjs && node --check src/pi-visualizer.mjs && node --check src/pi-visualizer-server.mjs && node --check src/pi-visualizer-shared.mjs && node --check src/index.mjs && node --check test/pi-heartbeat.test.mjs && node --check test/pi-lifecycle.test.mjs && node --check test/pi-role-models.test.mjs && node --check test/pi-flow.test.mjs && node --check test/pi-history.test.mjs && node --check test/pi-prompts.test.mjs && node --check test/pi-preflight.test.mjs && node --check test/pi-repo.test.mjs && node --check test/pi-sdk-supervisor.test.mjs && node --check test/pi-sdk-turn.test.mjs && node --check test/pi-telemetry.test.mjs && node --check test/pi-visualizer-shared.test.mjs && node --check test/fixtures/fake-pi.mjs && node --check test/fixtures/fake-pi-sdk.mjs",
23
- "test": "node --test test/pi-heartbeat.test.mjs test/pi-lifecycle.test.mjs test/pi-role-models.test.mjs test/pi-flow.test.mjs test/pi-history.test.mjs test/pi-prompts.test.mjs test/pi-preflight.test.mjs test/pi-repo.test.mjs test/pi-sdk-supervisor.test.mjs test/pi-sdk-turn.test.mjs test/pi-telemetry.test.mjs test/pi-visualizer-shared.test.mjs"
22
+ "check": "node --check src/cli.mjs && node --check src/pi-clear-history.mjs && node --check src/pi-client.mjs && node --check src/pi-config.mjs && node --check src/pi-debug-live.mjs && node --check src/pi-flow.mjs && node --check src/pi-heartbeat.mjs && node --check src/pi-history.mjs && node --check src/pi-preflight.mjs && node --check src/pi-prompts.mjs && node --check src/pi-repo.mjs && node --check src/pi-report.mjs && node --check src/pi-sdk-turn.mjs && node --check src/pi-supervisor.mjs && node --check src/pi-telemetry.mjs && node --check src/pi-visual-once.mjs && node --check src/pi-visual-review.mjs && node --check src/pi-visualizer.mjs && node --check src/pi-visualizer-server.mjs && node --check src/pi-visualizer-shared.mjs && node --check src/index.mjs && node --check test/pi-heartbeat.test.mjs && node --check test/pi-lifecycle.test.mjs && node --check test/pi-role-models.test.mjs && node --check test/pi-flow.test.mjs && node --check test/pi-history.test.mjs && node --check test/pi-prompts.test.mjs && node --check test/pi-preflight.test.mjs && node --check test/pi-repo.test.mjs && node --check test/pi-sdk-supervisor.test.mjs && node --check test/pi-sdk-turn.test.mjs && node --check test/pi-telemetry.test.mjs && node --check test/pi-visualizer-shared.test.mjs && node --check test/fixtures/fake-pi.mjs && node --check test/fixtures/fake-pi-sdk.mjs && node --check test/fixtures/fake-live-pi-sdk.mjs",
23
+ "test": "node --test test/pi-heartbeat.test.mjs test/pi-lifecycle.test.mjs test/pi-role-models.test.mjs test/pi-flow.test.mjs test/pi-history.test.mjs test/pi-prompts.test.mjs test/pi-preflight.test.mjs test/pi-repo.test.mjs test/pi-sdk-supervisor.test.mjs test/pi-sdk-turn.test.mjs test/pi-telemetry.test.mjs test/pi-visualizer-shared.test.mjs",
24
+ "debug:live-ui": "node src/cli.mjs debug-live --reset",
25
+ "dev:visualizer:ui": "npm --prefix visualizer-ui run dev",
26
+ "build:visualizer:ui": "npm --prefix visualizer-ui run build"
24
27
  },
25
28
  "files": [
26
29
  "src",
27
30
  "templates",
28
31
  "docs",
32
+ "visualizer-ui/dist",
29
33
  "pi.config.json",
30
34
  "SETUP.md",
31
35
  "README.md"
package/src/cli.mjs CHANGED
@@ -19,6 +19,7 @@ const COMMANDS = new Map([
19
19
  ['clear-history', 'pi-clear-history.mjs'],
20
20
  ['visual-once', 'pi-visual-once.mjs'],
21
21
  ['visualize', 'pi-visualizer.mjs'],
22
+ ['debug-live', 'pi-debug-live.mjs'],
22
23
  ['visual-review-worker', 'pi-visual-review.mjs'],
23
24
  ])
24
25
 
package/src/pi-client.mjs CHANGED
@@ -1,6 +1,11 @@
1
1
  import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
  import { randomUUID } from 'node:crypto'
4
+
5
+ const liveFeedWriteQueues = new Map()
6
+ const liveFeedSequences = new Map()
7
+ const MAX_LIVE_FEED_TEXT = 2000
8
+ const MAX_LIVE_FEED_SUMMARY = 600
4
9
  import {
5
10
  appendLog,
6
11
  writeTextFile,
@@ -39,12 +44,80 @@ async function writeAgentOutputSnapshot(config, content) {
39
44
  }
40
45
  }
41
46
 
47
+ function truncateText(value, maxChars) {
48
+ const text = String(value ?? '')
49
+ if (text.length <= maxChars) {
50
+ return text
51
+ }
52
+ return `${text.slice(0, maxChars - 16)}\n... [truncated]`
53
+ }
54
+
55
+ function summarizeValue(value, maxChars = MAX_LIVE_FEED_SUMMARY) {
56
+ if (value === null || value === undefined) {
57
+ return ''
58
+ }
59
+ if (typeof value === 'string') {
60
+ return truncateText(value, maxChars)
61
+ }
62
+ try {
63
+ return truncateText(JSON.stringify(value), maxChars)
64
+ } catch {
65
+ return truncateText(String(value), maxChars)
66
+ }
67
+ }
68
+
69
+ function sanitizeLiveFeedEvent(filePath, event) {
70
+ const nextSeq = (liveFeedSequences.get(filePath) ?? 0) + 1
71
+ liveFeedSequences.set(filePath, nextSeq)
72
+
73
+ const normalized = {
74
+ seq: nextSeq,
75
+ timestamp: String(event?.timestamp ?? new Date().toISOString()),
76
+ iteration: Number(event?.iteration ?? 0),
77
+ retryCount: Number(event?.retryCount ?? 0),
78
+ reason: String(event?.reason ?? ''),
79
+ phase: String(event?.phase ?? ''),
80
+ role: String(event?.role ?? ''),
81
+ kind: String(event?.kind ?? ''),
82
+ type: String(event?.type ?? 'event'),
83
+ toolName: String(event?.toolName ?? ''),
84
+ isError: event?.isError === true,
85
+ text: truncateText(event?.text ?? '', MAX_LIVE_FEED_TEXT),
86
+ }
87
+
88
+ const argsSummary = summarizeValue(event?.args)
89
+ const partialSummary = summarizeValue(event?.partialResult)
90
+ const resultSummary = summarizeValue(event?.result)
91
+ if (argsSummary !== '') {
92
+ normalized.argsSummary = argsSummary
93
+ }
94
+ if (partialSummary !== '') {
95
+ normalized.partialSummary = partialSummary
96
+ }
97
+ if (resultSummary !== '') {
98
+ normalized.resultSummary = resultSummary
99
+ }
100
+
101
+ return normalized
102
+ }
103
+
42
104
  async function appendLiveFeedEvent(config, event) {
43
105
  if (!config.runLiveFeedFile) {
44
106
  return
45
107
  }
46
- await fs.mkdir(path.dirname(config.runLiveFeedFile), { recursive: true })
47
- await fs.appendFile(config.runLiveFeedFile, `${JSON.stringify(event)}\n`, 'utf8')
108
+
109
+ const filePath = config.runLiveFeedFile
110
+ const previous = liveFeedWriteQueues.get(filePath) ?? Promise.resolve()
111
+ const next = previous
112
+ .catch(() => {})
113
+ .then(async () => {
114
+ const sanitized = sanitizeLiveFeedEvent(filePath, event)
115
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
116
+ await fs.appendFile(filePath, `${JSON.stringify(sanitized)}\n`, 'utf8')
117
+ })
118
+
119
+ liveFeedWriteQueues.set(filePath, next)
120
+ await next
48
121
  }
49
122
 
50
123
  async function runMockTurn({ config, sessionId, sessionFile, prompt, reason }) {
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs/promises'
4
+ import path from 'node:path'
5
+ import process from 'node:process'
6
+ import { spawn, execFileSync } from 'node:child_process'
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url))
10
+ const packageRoot = path.resolve(scriptDir, '..')
11
+ const cliFile = path.join(scriptDir, 'cli.mjs')
12
+ const fakePiFile = path.join(packageRoot, 'test', 'fixtures', 'fake-pi.mjs')
13
+ const fakeLiveSdkFile = path.join(packageRoot, 'test', 'fixtures', 'fake-live-pi-sdk.mjs')
14
+ const sandboxDir = path.join(packageRoot, '.pi-debug', 'live-ui')
15
+
16
+ function shellQuote(value) {
17
+ return JSON.stringify(String(value))
18
+ }
19
+
20
+ async function ensureRepo(cwd) {
21
+ try {
22
+ execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd, stdio: 'ignore' })
23
+ } catch {
24
+ execFileSync('git', ['init'], { cwd, stdio: 'ignore' })
25
+ execFileSync('git', ['config', 'user.name', 'PI Harness Debug'], { cwd, stdio: 'ignore' })
26
+ execFileSync('git', ['config', 'user.email', 'pi-harness-debug@example.com'], { cwd, stdio: 'ignore' })
27
+ }
28
+ }
29
+
30
+ async function seedFiles(cwd) {
31
+ await fs.mkdir(path.join(cwd, 'pi'), { recursive: true })
32
+ await fs.writeFile(path.join(cwd, 'TODOS.md'), [
33
+ '## Phase 1',
34
+ '',
35
+ '- [ ] Fake live task one',
36
+ '- [ ] Fake live task two',
37
+ '- [ ] Fake live task three',
38
+ '',
39
+ '## Phase 2',
40
+ '',
41
+ '- [ ] Fake live task four',
42
+ ].join('\n') + '\n', 'utf8')
43
+ await fs.writeFile(path.join(cwd, 'DEVELOPER.md'), 'Developer instructions for local visualizer debugging.\n', 'utf8')
44
+ await fs.writeFile(path.join(cwd, 'TESTER.md'), 'Tester instructions for local visualizer debugging.\n', 'utf8')
45
+ await fs.writeFile(path.join(cwd, 'pi.config.json'), `${JSON.stringify({
46
+ transport: 'sdk',
47
+ taskFile: 'TODOS.md',
48
+ developerInstructionsFile: 'DEVELOPER.md',
49
+ testerInstructionsFile: 'TESTER.md',
50
+ piCli: fakePiFile,
51
+ piModel: 'fake-model',
52
+ roleModels: {
53
+ developer: 'fake-model',
54
+ developerRetry: 'fake-model',
55
+ developerFix: 'fake-model',
56
+ tester: 'fake-model',
57
+ testerCommit: 'fake-model',
58
+ },
59
+ testCommand: `${shellQuote(process.execPath)} -e ${shellQuote('setTimeout(()=>process.exit(0), 250)')}`,
60
+ streamTerminal: true,
61
+ continueAfterSeconds: 3600,
62
+ noEventTimeoutSeconds: 3600,
63
+ toolContinueAfterSeconds: 3600,
64
+ toolNoEventTimeoutSeconds: 3600,
65
+ sleepBetweenSeconds: 1,
66
+ maxIterations: 20,
67
+ }, null, 2)}\n`, 'utf8')
68
+ }
69
+
70
+ async function ensureInitialCommit(cwd) {
71
+ try {
72
+ execFileSync('git', ['rev-parse', 'HEAD'], { cwd, stdio: 'ignore' })
73
+ } catch {
74
+ execFileSync('git', ['add', '.'], { cwd, stdio: 'ignore' })
75
+ execFileSync('git', ['commit', '-m', 'chore(debug): seed fake live sandbox'], { cwd, stdio: 'ignore' })
76
+ }
77
+ }
78
+
79
+ async function main() {
80
+ const reset = process.argv.includes('--reset')
81
+ if (reset) {
82
+ await fs.rm(sandboxDir, { recursive: true, force: true })
83
+ }
84
+
85
+ await fs.mkdir(sandboxDir, { recursive: true })
86
+ await ensureRepo(sandboxDir)
87
+ await seedFiles(sandboxDir)
88
+ await ensureInitialCommit(sandboxDir)
89
+
90
+ process.stdout.write(`PI debug sandbox: ${sandboxDir}\n`)
91
+ process.stdout.write(`Using fake live SDK fixture: ${fakeLiveSdkFile}\n`)
92
+
93
+ const child = spawn(process.execPath, [cliFile, 'run'], {
94
+ cwd: sandboxDir,
95
+ env: {
96
+ ...process.env,
97
+ PI_CONFIG_FILE: 'pi.config.json',
98
+ PI_SDK_MODULE: fakeLiveSdkFile,
99
+ PI_VISUALIZER_HOST: process.env.PI_VISUALIZER_HOST || '127.0.0.1',
100
+ PI_VISUALIZER_PORT: process.env.PI_VISUALIZER_PORT || '4317',
101
+ },
102
+ stdio: 'inherit',
103
+ })
104
+
105
+ child.on('exit', (code, signal) => {
106
+ if (signal) {
107
+ process.exitCode = 128
108
+ return
109
+ }
110
+ process.exitCode = code ?? 1
111
+ })
112
+ }
113
+
114
+ main().catch((error) => {
115
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error))
116
+ process.exitCode = 1
117
+ })
@@ -2,6 +2,8 @@ import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
4
  const CSV_HEADER = 'timestamp,run_id,iteration,phase,kind,status,transport,session_id,timed_out,exit_code,duration_seconds,commit_before,commit_after,repo_changed,changed_files_count,verification_status,retry_count,role,model,tool_calls,tool_errors,message_updates,stop_reason,loop_detected,loop_signature,tester_verdict,commit_plan_found,terminal_reason,risk_warnings,notes\n'
5
+ const DEFAULT_JSONL_TAIL_BYTES = 512 * 1024
6
+ const JSONL_TAIL_CHUNK_BYTES = 64 * 1024
5
7
 
6
8
  function csvEscape(value) {
7
9
  const text = String(value ?? '')
@@ -95,15 +97,78 @@ export async function appendTelemetry(config, event) {
95
97
  }
96
98
  }
97
99
 
100
+ function parseJsonlLines(raw, { dropFirstLine = false, maxItems = Infinity } = {}) {
101
+ const lines = raw.split('\n')
102
+ if (dropFirstLine && lines.length > 0) {
103
+ lines.shift()
104
+ }
105
+
106
+ const items = []
107
+ for (const line of lines) {
108
+ const trimmed = line.trim()
109
+ if (trimmed === '') {
110
+ continue
111
+ }
112
+ try {
113
+ items.push(JSON.parse(trimmed))
114
+ } catch {
115
+ // Ignore partial/truncated JSONL records while file is actively being appended.
116
+ }
117
+ }
118
+
119
+ return Number.isFinite(maxItems) ? items.slice(-maxItems) : items
120
+ }
121
+
122
+ export async function readJsonlTail(filePath, options = {}) {
123
+ const maxItems = Number.isFinite(Number(options.maxItems)) ? Number(options.maxItems) : 200
124
+ const maxBytes = Number.isFinite(Number(options.maxBytes)) ? Number(options.maxBytes) : DEFAULT_JSONL_TAIL_BYTES
125
+
126
+ let handle
127
+ try {
128
+ handle = await fs.open(filePath, 'r')
129
+ const stat = await handle.stat()
130
+ if (!Number.isFinite(stat.size) || stat.size <= 0) {
131
+ return []
132
+ }
133
+
134
+ let position = stat.size
135
+ let text = ''
136
+ let newlineCount = 0
137
+
138
+ while (position > 0 && Buffer.byteLength(text, 'utf8') < maxBytes && newlineCount <= (maxItems + 1)) {
139
+ const remainingBudget = maxBytes - Buffer.byteLength(text, 'utf8')
140
+ const chunkSize = Math.min(JSONL_TAIL_CHUNK_BYTES, position, remainingBudget)
141
+ if (chunkSize <= 0) {
142
+ break
143
+ }
144
+
145
+ position -= chunkSize
146
+ const buffer = Buffer.alloc(chunkSize)
147
+ const { bytesRead } = await handle.read(buffer, 0, chunkSize, position)
148
+ text = buffer.subarray(0, bytesRead).toString('utf8') + text
149
+ newlineCount = (text.match(/\n/g) ?? []).length
150
+ }
151
+
152
+ return parseJsonlLines(text, {
153
+ dropFirstLine: position > 0,
154
+ maxItems,
155
+ })
156
+ } catch {
157
+ return []
158
+ } finally {
159
+ await handle?.close().catch(() => {})
160
+ }
161
+ }
162
+
98
163
  export async function readTelemetry(config) {
99
164
  try {
100
165
  const raw = await fs.readFile(config.telemetryJsonl, 'utf8')
101
- return raw
102
- .split('\n')
103
- .map((line) => line.trim())
104
- .filter(Boolean)
105
- .map((line) => JSON.parse(line))
166
+ return parseJsonlLines(raw)
106
167
  } catch {
107
168
  return []
108
169
  }
109
170
  }
171
+
172
+ export async function readTelemetryTail(config, maxItems = 200, maxBytes = DEFAULT_JSONL_TAIL_BYTES) {
173
+ return await readJsonlTail(config.telemetryJsonl, { maxItems, maxBytes })
174
+ }