@pasko70/pibo 1.7.2 → 1.7.3
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 +183 -0
- package/dist/apps/chat/chat-trace-helpers.js +3 -1
- package/dist/apps/chat/trace-v2.js +9 -4
- package/dist/apps/chat/trace.js +126 -0
- package/dist/apps/chat/web-app.js +160 -32
- package/dist/apps/chat-ui/assets/{dist-eRtG3vUM.js → dist-0VZenZBV.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Cfyo-Bgw.js → dist-BMjRcU4S.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BXT6rjJn.js → dist-Bbb6cmLt.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DjmyW251.js → dist-BxwcyegP.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Jc0njXt3.js → dist-CESPWSQ-.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-8eLx-6HY.js → dist-CQt6ULIj.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CCHtiwhr.js → dist-CjbsWko8.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CxWQqjbA.js → dist-DSDqYG1O.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Ddn6n4ge.js → dist-DYqe5Zvc.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CCt9U3Or.js → dist-DnhVsXHZ.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-cyStkSiF.js → dist-Ih0wkIep.js} +1 -1
- package/dist/apps/chat-ui/assets/index-j2BTjmvp.js +166 -0
- package/dist/apps/chat-ui/index.html +1 -1
- package/dist/apps/chat-vscode-web/assets/{index-CfeNUaRh.js → index-BHL2ZJCu.js} +5 -5
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.7.3.vsix +0 -0
- package/dist/shared/trace-page-merge.js +40 -0
- package/docs/README.md +23 -0
- package/docs/ops/install-developer-host.md +112 -0
- package/docs/ops/install-user-host.md +96 -0
- package/docs/ops/upgrade-user-to-developer-host.md +69 -0
- package/docs/ops/vscode-extension-release.md +160 -0
- package/package.json +93 -93
- package/dist/apps/chat-ui/assets/index-COp5Hme0.js +0 -166
package/README.md
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# pibo
|
|
2
|
+
|
|
3
|
+
Pibo is a thin product boundary around Pi Coding Agent. Pi remains the inner engine for model turns, tools, streaming, sessions, and compaction. Pibo adds profiles, plugins, channels, local gateways, web auth, and operator tooling.
|
|
4
|
+
|
|
5
|
+
The default posture is intentionally powerful: Pibo is built for agentic coding and server operation, not for a restrictive sandbox. Install and run it as the Linux user that should own its state and credentials.
|
|
6
|
+
|
|
7
|
+
## Supported platforms
|
|
8
|
+
|
|
9
|
+
Pibo is Linux-first. The supported hosts are:
|
|
10
|
+
|
|
11
|
+
- **Linux** (any modern distribution) — primary target
|
|
12
|
+
- **macOS** — supported, same code path as Linux
|
|
13
|
+
- **Windows via WSL2** — fully supported; see [docs/guides/pibo-on-windows-via-wsl.md](./docs/guides/pibo-on-windows-via-wsl.md)
|
|
14
|
+
- **Native Windows** — not supported; `pibo setup doctor` and `pibo setup user-host` will print a clear error pointing to the WSL guide
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Pibo is distributed as an npm package:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g @pasko70/pibo
|
|
22
|
+
pibo --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The current package declares Node.js 24+. Some Linux distributions still ship Node 22 through `apt`, so install a recent Node release first when needed. A user-local Node/npm setup through `nvm`, `fnm`, or a user-writable npm prefix works well.
|
|
26
|
+
|
|
27
|
+
For a user-local global npm prefix:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
mkdir -p ~/.local
|
|
31
|
+
npm config set prefix ~/.local
|
|
32
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
33
|
+
npm install -g @pasko70/pibo
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## State and user paths
|
|
37
|
+
|
|
38
|
+
Pibo stores product-wide state under:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
${PIBO_HOME:-~/.pibo}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That means the executing Linux user owns the installation state:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
alice -> /home/alice/.pibo
|
|
48
|
+
pibo -> /home/pibo/.pibo
|
|
49
|
+
root -> /root/.pibo
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
There is no requirement to run as `root`. If you SSH as root and run Pibo, state naturally lands in `/root/.pibo`. If you create or use a dedicated user, state lands in that user's home. This is the preferred way to keep SSH keys, config, logs, and sessions separated.
|
|
53
|
+
|
|
54
|
+
Workspace-scoped files are separate and live under the active workspace:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
<workspace>/.pibo
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Examples include custom prompts and Pi package registrations.
|
|
61
|
+
|
|
62
|
+
## Server notes
|
|
63
|
+
|
|
64
|
+
A fresh server install usually needs only three decisions:
|
|
65
|
+
|
|
66
|
+
1. Choose the Linux user that should own Pibo.
|
|
67
|
+
2. Install Node.js 24+ for that user or system-wide.
|
|
68
|
+
3. Install Pibo through npm.
|
|
69
|
+
|
|
70
|
+
Use the setup planner to keep the first run simple:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pibo setup doctor --domain pibo.example.com --expected-ip <server-ip>
|
|
74
|
+
pibo setup user-host --domain pibo.example.com --print-files
|
|
75
|
+
pibo setup user-host --domain pibo.example.com --write-to /tmp/pibo-setup
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This is the normal user path: one gateway, one `PIBO_HOME`, no required Docker, no dev gateway, and no GitHub App setup. After reviewing staged files, use `--apply --yes` to write the generated systemd/Caddy files.
|
|
79
|
+
|
|
80
|
+
Developer hosts are opt-in and add production/dev separation plus Docker compute workers:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pibo setup developer-host \
|
|
84
|
+
--origin git@github.com:<your-fork>/pibo.git \
|
|
85
|
+
--prod-domain pibo.example.com \
|
|
86
|
+
--dev-domain dev.pibo.example.com \
|
|
87
|
+
--print-files
|
|
88
|
+
|
|
89
|
+
pibo setup developer-host \
|
|
90
|
+
--origin git@github.com:<your-fork>/pibo.git \
|
|
91
|
+
--prod-domain pibo.example.com \
|
|
92
|
+
--dev-domain dev.pibo.example.com \
|
|
93
|
+
--write-to /tmp/pibo-setup
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
See `docs/ops/install-user-host.md`, `docs/ops/install-developer-host.md`, and `docs/ops/upgrade-user-to-developer-host.md`. Developer-host services are source-pinned so production and dev do not fight over one global `pibo` symlink. Docker and swap are developer-host prerequisites only; verify them with `pibo setup doctor --require-docker --min-swap-gb 8`.
|
|
97
|
+
|
|
98
|
+
If the agent should be able to perform server administration, give that Linux user the required sudo or Docker permissions explicitly. Pibo does not need a special onboarding user to work correctly; normal Unix ownership is enough.
|
|
99
|
+
|
|
100
|
+
## Docker notes
|
|
101
|
+
|
|
102
|
+
Docker is optional. If you run Pibo in a container, avoid hard-coding `/root` unless the container is intentionally root-owned. Prefer mapping the host user's state into the container user's home:
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
$HOME/.pibo -> /home/pibo/.pibo
|
|
106
|
+
$HOME/code -> /workspace/code
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Set `HOME` and `PIBO_HOME` consistently inside the container so the same path rule applies:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
HOME=/home/pibo
|
|
113
|
+
PIBO_HOME=/home/pibo/.pibo
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Web gateway auth
|
|
117
|
+
|
|
118
|
+
`pibo gateway:web` starts the authenticated web runtime. It requires Better Auth configuration before production use. This step is intentionally not automated because you must create/select the Google OAuth client and allowed user list:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
pibo config set auth.baseURL https://your-host.example
|
|
122
|
+
pibo config set auth.secret <at-least-32-characters>
|
|
123
|
+
pibo config set auth.googleClientId <google-client-id>
|
|
124
|
+
pibo config set auth.googleClientSecret <google-client-secret>
|
|
125
|
+
pibo config set auth.allowedEmails you@example.com
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
For local loopback testing, `auth.baseURL` can be `http://localhost:4788`.
|
|
129
|
+
|
|
130
|
+
## Development from source
|
|
131
|
+
|
|
132
|
+
Use source installs for development, not normal usage:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
git clone git@github.com:Pascapone-server/pibo.git
|
|
136
|
+
cd pibo
|
|
137
|
+
npm install
|
|
138
|
+
npm run build
|
|
139
|
+
npm run start -- --help
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Useful scripts:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm run dev -- --help
|
|
146
|
+
npm run profile -- codex
|
|
147
|
+
npm run tui:routed -- codex
|
|
148
|
+
npm run gateway
|
|
149
|
+
npm run gateway:web
|
|
150
|
+
npm test
|
|
151
|
+
npm run typecheck
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Main CLI areas
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
pibo config # local config under ${PIBO_HOME:-~/.pibo}/config.json
|
|
158
|
+
pibo mcp # discover and call configured MCP servers
|
|
159
|
+
pibo tools # install and inspect curated external CLI tools
|
|
160
|
+
pibo pi-packages # register Pi Coding Agent packages
|
|
161
|
+
pibo debug # inspect local Pibo data stores
|
|
162
|
+
pibo setup # plan user-host installs and developer-host upgrades
|
|
163
|
+
pibo profile # inspect runtime profiles
|
|
164
|
+
pibo tui # start the direct Pi TUI
|
|
165
|
+
pibo tui:routed # start the routed Pibo TUI
|
|
166
|
+
pibo gateway # local gateway runtime
|
|
167
|
+
pibo gateway:web # authenticated web gateway runtime
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Further docs
|
|
171
|
+
|
|
172
|
+
- `docs/architecture.md` describes runtime architecture and boundaries.
|
|
173
|
+
- `docs/mcp.md` documents MCP configuration and commands.
|
|
174
|
+
- `docs/tools.md` documents curated external CLI tools.
|
|
175
|
+
- `docs/pi-packages.md` documents Pi package registration.
|
|
176
|
+
- `docs/chat-rooms-event-log.md` documents Chat Web rooms and durable event storage.
|
|
177
|
+
- `docs/progress.md` is the short implementation status snapshot.
|
|
178
|
+
|
|
179
|
+
## Philosophy
|
|
180
|
+
|
|
181
|
+
Keep Pibo thin. Pi Coding Agent should remain the inner engine; Pibo owns only the product boundary: profiles, plugins, channels, routing, auth, policy, and operator tooling.
|
|
182
|
+
|
|
183
|
+
Optional integrations should stay outside the core package until installed. MCP servers, Python virtual environments, external CLIs, and user skills are configured on demand rather than bundled into every runtime.
|
|
@@ -75,10 +75,12 @@ export function annotateTracePage(trace, events, input) {
|
|
|
75
75
|
eventLimit: input.pageSize,
|
|
76
76
|
pageSize: input.pageSize,
|
|
77
77
|
beforeSequence: input.beforeSequence,
|
|
78
|
+
beforeCursor: input.beforeCursor,
|
|
78
79
|
firstEventSequence,
|
|
79
80
|
lastEventSequence,
|
|
80
81
|
nextBeforeSequence: firstEventSequence,
|
|
81
|
-
|
|
82
|
+
nextBeforeCursor: input.nextBeforeCursor ?? (firstEventSequence !== undefined ? String(firstEventSequence) : undefined),
|
|
83
|
+
hasOlderEvents: input.hasOlderEvents ?? (firstEventSequence !== undefined ? firstEventSequence > 1 : false),
|
|
82
84
|
};
|
|
83
85
|
}
|
|
84
86
|
export function setTraceCache(cache, key, trace, maxEntries, maxBytes = TRACE_CACHE_MAX_BYTES) {
|
|
@@ -20,6 +20,10 @@ export function traceTimelinePageFromView(input) {
|
|
|
20
20
|
limit: Math.max(1, Math.min(input.limit, TRACE_V2_MAX_TIMELINE_LIMIT)),
|
|
21
21
|
fromTail: input.fromTail,
|
|
22
22
|
});
|
|
23
|
+
const transcriptTailCursor = input.fromTail ? input.transcriptTailCursor : undefined;
|
|
24
|
+
const hasOlder = input.trace.hasOlderEvents === true || transcriptTailCursor !== undefined;
|
|
25
|
+
const nextBeforeCursor = transcriptTailCursor ?? (hasOlder ? input.trace.nextBeforeCursor : undefined);
|
|
26
|
+
const nextBeforeSequence = transcriptTailCursor ? undefined : (hasOlder ? input.trace.nextBeforeSequence : undefined);
|
|
23
27
|
let page = {
|
|
24
28
|
piboSessionId: input.trace.piboSessionId,
|
|
25
29
|
piSessionId: input.trace.piSessionId,
|
|
@@ -28,9 +32,9 @@ export function traceTimelinePageFromView(input) {
|
|
|
28
32
|
latestStreamId: input.trace.latestStreamId,
|
|
29
33
|
projectionStatus: "ready",
|
|
30
34
|
cursor: {
|
|
31
|
-
before:
|
|
35
|
+
before: nextBeforeCursor ?? (nextBeforeSequence !== undefined ? String(nextBeforeSequence) : undefined),
|
|
32
36
|
after: input.trace.lastEventSequence !== undefined ? String(input.trace.lastEventSequence) : undefined,
|
|
33
|
-
hasOlder
|
|
37
|
+
hasOlder,
|
|
34
38
|
hasNewer: false,
|
|
35
39
|
},
|
|
36
40
|
nodes,
|
|
@@ -43,8 +47,9 @@ export function traceTimelinePageFromView(input) {
|
|
|
43
47
|
pageSize: input.trace.pageSize,
|
|
44
48
|
firstEventSequence: input.trace.firstEventSequence,
|
|
45
49
|
lastEventSequence: input.trace.lastEventSequence,
|
|
46
|
-
nextBeforeSequence
|
|
47
|
-
|
|
50
|
+
nextBeforeSequence,
|
|
51
|
+
nextBeforeCursor,
|
|
52
|
+
hasOlderEvents: hasOlder,
|
|
48
53
|
};
|
|
49
54
|
while (Buffer.byteLength(JSON.stringify(page), "utf8") > byteLimit && page.nodes.length > 1) {
|
|
50
55
|
page = {
|
package/dist/apps/chat/trace.js
CHANGED
|
@@ -8,6 +8,8 @@ import { isChatWebSessionArchived } from "./session-metadata.js";
|
|
|
8
8
|
import { workflowSessionKindFromMetadata } from "../../sessions/workflow-session-kind.js";
|
|
9
9
|
export { compareTraceNodes, sortTraceNodes, nestTraceNodes, flattenTraceNodes, mapTraceNodesById, buildTraceViewFromEvents, traceNodesFromEntries, } from "../../shared/trace-engine.js";
|
|
10
10
|
export const TRACE_TRANSCRIPT_TAIL_MAX_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
export const TRACE_TRANSCRIPT_HISTORY_PAGE_MAX_BYTES = 512 * 1024;
|
|
12
|
+
export const TRACE_TRANSCRIPT_HISTORY_SCAN_MAX_BYTES = 16 * 1024 * 1024;
|
|
11
13
|
export async function loadPiSessionMetadata(session, cwd = process.cwd()) {
|
|
12
14
|
const piSession = await findPiSession(session, cwd);
|
|
13
15
|
return metadataFromPiSession(piSession);
|
|
@@ -133,6 +135,9 @@ export async function buildTraceView(input) {
|
|
|
133
135
|
includeRawEvents: input.includeRawEvents,
|
|
134
136
|
rawEventsLimit: input.rawEventsLimit,
|
|
135
137
|
});
|
|
138
|
+
if (input.transcriptOrderOffset && input.transcriptOrderOffset > 0) {
|
|
139
|
+
offsetTranscriptTraceOrder(view.nodes, input.transcriptOrderOffset);
|
|
140
|
+
}
|
|
136
141
|
annotateForkableUserMessageNodes(view.nodes, allEntries);
|
|
137
142
|
return {
|
|
138
143
|
...view,
|
|
@@ -324,6 +329,127 @@ export function readTailEntries(path, maxBytes = TRACE_TRANSCRIPT_TAIL_MAX_BYTES
|
|
|
324
329
|
closeSync(fd);
|
|
325
330
|
}
|
|
326
331
|
}
|
|
332
|
+
export function readTranscriptHistoryPage(path, input = {}) {
|
|
333
|
+
if (!existsSync(path))
|
|
334
|
+
return { entries: [], hasOlder: false, scannedBytes: 0, startByte: 0, endByte: 0 };
|
|
335
|
+
const stats = statSync(path);
|
|
336
|
+
const fileSize = Math.max(0, stats.size);
|
|
337
|
+
const initialEnd = input.beforeByte === undefined
|
|
338
|
+
? fileSize
|
|
339
|
+
: Math.max(0, Math.min(input.beforeByte, fileSize));
|
|
340
|
+
const limit = Math.max(1, Math.min(input.limit ?? 50, 500));
|
|
341
|
+
const pageBytes = Math.max(1024, Math.min(input.pageBytes ?? TRACE_TRANSCRIPT_HISTORY_PAGE_MAX_BYTES, TRACE_TRANSCRIPT_HISTORY_SCAN_MAX_BYTES));
|
|
342
|
+
const maxScanBytes = Math.max(pageBytes, Math.min(input.maxScanBytes ?? TRACE_TRANSCRIPT_HISTORY_SCAN_MAX_BYTES, 32 * 1024 * 1024));
|
|
343
|
+
const beforeTime = input.beforeTimestamp ? Date.parse(input.beforeTimestamp) : undefined;
|
|
344
|
+
const entries = [];
|
|
345
|
+
let cursorEnd = initialEnd;
|
|
346
|
+
let scannedBytes = 0;
|
|
347
|
+
let nextBeforeByte;
|
|
348
|
+
while (cursorEnd > 0 && entries.length < limit && scannedBytes < maxScanBytes) {
|
|
349
|
+
const chunkStart = Math.max(0, cursorEnd - pageBytes);
|
|
350
|
+
const chunk = readUtf8Range(path, chunkStart, cursorEnd);
|
|
351
|
+
scannedBytes += cursorEnd - chunkStart;
|
|
352
|
+
const records = transcriptLineRecords(chunk, chunkStart, cursorEnd);
|
|
353
|
+
for (let index = records.length - 1; index >= 0; index -= 1) {
|
|
354
|
+
const record = records[index];
|
|
355
|
+
const parsed = parseTranscriptLine(record.text);
|
|
356
|
+
for (let entryIndex = parsed.length - 1; entryIndex >= 0; entryIndex -= 1) {
|
|
357
|
+
const entry = parsed[entryIndex];
|
|
358
|
+
if (beforeTime !== undefined && entryTimestampMs(entry) >= beforeTime)
|
|
359
|
+
continue;
|
|
360
|
+
entries.push({ entry, startByte: record.startByte });
|
|
361
|
+
if (entries.length >= limit) {
|
|
362
|
+
nextBeforeByte = record.startByte;
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (entries.length >= limit)
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
cursorEnd = chunkStart;
|
|
370
|
+
}
|
|
371
|
+
if (nextBeforeByte === undefined)
|
|
372
|
+
nextBeforeByte = cursorEnd > 0 ? cursorEnd : undefined;
|
|
373
|
+
return {
|
|
374
|
+
entries: entries.reverse().map((item) => item.entry),
|
|
375
|
+
nextBeforeByte,
|
|
376
|
+
hasOlder: nextBeforeByte !== undefined && nextBeforeByte > 0,
|
|
377
|
+
scannedBytes,
|
|
378
|
+
startByte: nextBeforeByte ?? 0,
|
|
379
|
+
endByte: initialEnd,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function readUtf8Range(path, start, end) {
|
|
383
|
+
const length = Math.max(0, end - start);
|
|
384
|
+
if (length === 0)
|
|
385
|
+
return "";
|
|
386
|
+
const buffer = Buffer.alloc(length);
|
|
387
|
+
const fd = openSync(path, "r");
|
|
388
|
+
try {
|
|
389
|
+
const bytesRead = readSync(fd, buffer, 0, length, start);
|
|
390
|
+
return buffer.subarray(0, bytesRead).toString("utf8");
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
closeSync(fd);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function transcriptLineRecords(content, startByte, endByte) {
|
|
397
|
+
let text = content;
|
|
398
|
+
let byteOffset = startByte;
|
|
399
|
+
if (startByte > 0) {
|
|
400
|
+
const firstNewline = text.indexOf("\n");
|
|
401
|
+
if (firstNewline < 0)
|
|
402
|
+
return [];
|
|
403
|
+
const skipped = text.slice(0, firstNewline + 1);
|
|
404
|
+
byteOffset += Buffer.byteLength(skipped, "utf8");
|
|
405
|
+
text = text.slice(firstNewline + 1);
|
|
406
|
+
}
|
|
407
|
+
if (endByte > startByte && text && !text.endsWith("\n")) {
|
|
408
|
+
const lastNewline = text.lastIndexOf("\n");
|
|
409
|
+
if (lastNewline >= 0)
|
|
410
|
+
text = text.slice(0, lastNewline + 1);
|
|
411
|
+
}
|
|
412
|
+
const records = [];
|
|
413
|
+
const segments = text.match(/[^\n]*(?:\n|$)/g) ?? [];
|
|
414
|
+
for (const segment of segments) {
|
|
415
|
+
if (!segment)
|
|
416
|
+
continue;
|
|
417
|
+
const lineStart = byteOffset;
|
|
418
|
+
byteOffset += Buffer.byteLength(segment, "utf8");
|
|
419
|
+
const line = segment.replace(/\r?\n$/, "");
|
|
420
|
+
if (line.trim())
|
|
421
|
+
records.push({ text: line, startByte: lineStart });
|
|
422
|
+
}
|
|
423
|
+
return records;
|
|
424
|
+
}
|
|
425
|
+
function parseTranscriptLine(line) {
|
|
426
|
+
try {
|
|
427
|
+
return parseSessionEntries(`${line}\n`).filter((entry) => entry.type !== "session");
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function entryTimestampMs(entry) {
|
|
434
|
+
const timestamp = entry.timestamp;
|
|
435
|
+
if (typeof timestamp !== "string")
|
|
436
|
+
return Number.POSITIVE_INFINITY;
|
|
437
|
+
const parsed = Date.parse(timestamp);
|
|
438
|
+
return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
439
|
+
}
|
|
440
|
+
function offsetTranscriptTraceOrder(nodes, offset) {
|
|
441
|
+
for (const node of nodes) {
|
|
442
|
+
if (node.source === "transcript" && node.orderKey) {
|
|
443
|
+
const transcriptIndex = node.orderKey.transcriptIndex;
|
|
444
|
+
node.orderKey = {
|
|
445
|
+
...node.orderKey,
|
|
446
|
+
turnSeq: node.orderKey.turnSeq + offset,
|
|
447
|
+
transcriptIndex: transcriptIndex === undefined ? undefined : transcriptIndex + offset,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
offsetTranscriptTraceOrder(node.children, offset);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
327
453
|
function annotateForkableUserMessageNodes(nodes, entries) {
|
|
328
454
|
const candidates = userMessageForkCandidates(entries);
|
|
329
455
|
if (!candidates.length)
|
|
@@ -6,7 +6,7 @@ import { OutputCompactor } from "./output-compactor.js";
|
|
|
6
6
|
import { isLiveOnlyOutputEvent, isPersistableOutputEvent } from "./output-event-policy.js";
|
|
7
7
|
import { chatRoomIdFromMetadata, isDefaultPiboRoom, isPiboRoomArchived, roomWorkspaceFromMetadata, withChatRoomId, withPiboRoomWorkspace, } from "./types/rooms.js";
|
|
8
8
|
import { chatStreamFramesFromOutputEvent, createChatStreamState, nextTransientChatStreamFrameId } from "./stream.js";
|
|
9
|
-
import { buildSessionNodes, buildTraceView, createTraceViewVersion, loadPiSessionMetadata, readTailEntries } from "./trace.js";
|
|
9
|
+
import { buildSessionNodes, buildTraceView, createTraceViewVersion, loadPiSessionMetadata, readTailEntries, readTranscriptHistoryPage } from "./trace.js";
|
|
10
10
|
import { DEFAULT_TRACE_EVENTS_PAGE_SIZE, annotateTracePage, etagForVersion, estimateTraceViewBytes, traceCacheEstimatedBytes, liveSnapshotVersion, requestMatchesVersion, setTraceCache, traceCacheKey, withLiveSnapshots, withRawTraceTail, } from "./chat-trace-helpers.js";
|
|
11
11
|
import { TRACE_V2_DEFAULT_TIMELINE_LIMIT, TRACE_V2_MAX_TIMELINE_LIMIT, TRACE_V2_PAYLOAD_DEFAULT_LIMIT_BYTES, TRACE_V2_PAYLOAD_MAX_LIMIT_BYTES, TRACE_V2_RAW_EVENTS_DEFAULT_LIMIT, TRACE_V2_RAW_EVENTS_MAX_LIMIT, TRACE_V2_TIMELINE_HARD_BYTES, parseTracePayloadRef, readTracePayloadChunk, traceRawEventsPageFromEvents, traceTimelinePageFromView, } from "./trace-v2.js";
|
|
12
12
|
import { isChatWebSessionArchived } from "./session-metadata.js";
|
|
@@ -2254,14 +2254,76 @@ function recordTransientReplayEvent(state, event) {
|
|
|
2254
2254
|
function parseTimelineBeforeCursor(url) {
|
|
2255
2255
|
const before = parseOptionalPositiveIntSearchParam(url, "beforeSequence");
|
|
2256
2256
|
if (before !== undefined)
|
|
2257
|
-
return before;
|
|
2257
|
+
return { kind: "event", beforeSequence: before, raw: String(before) };
|
|
2258
2258
|
const cursor = url.searchParams.get("before") ?? url.searchParams.get("cursor");
|
|
2259
2259
|
if (!cursor || cursor === "tail")
|
|
2260
|
-
return
|
|
2260
|
+
return { kind: "tail" };
|
|
2261
|
+
const transcript = parseTranscriptTimelineCursor(cursor);
|
|
2262
|
+
if (transcript)
|
|
2263
|
+
return transcript;
|
|
2261
2264
|
const parsed = Number.parseInt(cursor, 10);
|
|
2262
2265
|
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
2263
|
-
throw new PiboWebHttpError("Trace cursor must be tail or a positive sequence", 400);
|
|
2264
|
-
return parsed;
|
|
2266
|
+
throw new PiboWebHttpError("Trace cursor must be tail, transcript, or a positive sequence", 400);
|
|
2267
|
+
return { kind: "event", beforeSequence: parsed, raw: String(parsed) };
|
|
2268
|
+
}
|
|
2269
|
+
function parseTranscriptTimelineCursor(cursor) {
|
|
2270
|
+
if (!cursor.startsWith("transcript:"))
|
|
2271
|
+
return undefined;
|
|
2272
|
+
const [, byteValue, encodedTimestamp] = cursor.split(":");
|
|
2273
|
+
const beforeByte = Number.parseInt(byteValue ?? "", 10);
|
|
2274
|
+
if (!Number.isFinite(beforeByte) || beforeByte < 0)
|
|
2275
|
+
throw new PiboWebHttpError("Invalid transcript trace cursor", 400);
|
|
2276
|
+
let beforeTimestamp;
|
|
2277
|
+
if (encodedTimestamp) {
|
|
2278
|
+
try {
|
|
2279
|
+
const decoded = Buffer.from(encodedTimestamp, "base64url").toString("utf8");
|
|
2280
|
+
if (decoded)
|
|
2281
|
+
beforeTimestamp = decoded;
|
|
2282
|
+
}
|
|
2283
|
+
catch {
|
|
2284
|
+
throw new PiboWebHttpError("Invalid transcript trace cursor", 400);
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
return { kind: "transcript", beforeByte, beforeTimestamp, raw: cursor };
|
|
2288
|
+
}
|
|
2289
|
+
function encodeTranscriptTimelineCursor(beforeByte, beforeTimestamp) {
|
|
2290
|
+
const encodedTimestamp = beforeTimestamp ? Buffer.from(beforeTimestamp, "utf8").toString("base64url") : "";
|
|
2291
|
+
return `transcript:${Math.max(0, Math.floor(beforeByte))}:${encodedTimestamp}`;
|
|
2292
|
+
}
|
|
2293
|
+
function compactionCutoffTimestamp(events) {
|
|
2294
|
+
const compaction = events.find((event) => event.type === "execution_result" && event.payload.type === "execution_result" && event.payload.action === "compact")
|
|
2295
|
+
?? events.find((event) => event.type === "compaction_end" || event.type === "compaction_start");
|
|
2296
|
+
return compaction?.createdAt;
|
|
2297
|
+
}
|
|
2298
|
+
function originTranscriptTailCursor(input) {
|
|
2299
|
+
if (!input.session.originId)
|
|
2300
|
+
return undefined;
|
|
2301
|
+
const sessionPath = input.metadata?.sessionPath;
|
|
2302
|
+
const sessionSize = input.metadata?.sessionSize;
|
|
2303
|
+
if (!sessionPath || !sessionSize || sessionSize <= 0)
|
|
2304
|
+
return undefined;
|
|
2305
|
+
const firstVisibleTranscript = firstVisibleTailTranscriptNode(input.trace.nodes, input.limit);
|
|
2306
|
+
if (!firstVisibleTranscript?.startedAt)
|
|
2307
|
+
return undefined;
|
|
2308
|
+
const probe = readTranscriptHistoryPage(sessionPath, {
|
|
2309
|
+
beforeByte: sessionSize,
|
|
2310
|
+
beforeTimestamp: firstVisibleTranscript.startedAt,
|
|
2311
|
+
limit: 1,
|
|
2312
|
+
});
|
|
2313
|
+
if (probe.entries.length === 0)
|
|
2314
|
+
return undefined;
|
|
2315
|
+
return encodeTranscriptTimelineCursor(sessionSize, firstVisibleTranscript.startedAt);
|
|
2316
|
+
}
|
|
2317
|
+
function firstVisibleTailTranscriptNode(nodes, limit) {
|
|
2318
|
+
const flat = [];
|
|
2319
|
+
const visit = (items) => {
|
|
2320
|
+
for (const item of items) {
|
|
2321
|
+
flat.push(item);
|
|
2322
|
+
visit(item.children);
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
visit(nodes);
|
|
2326
|
+
return flat.slice(-Math.max(1, limit)).find((node) => node.source === "transcript" && node.startedAt);
|
|
2265
2327
|
}
|
|
2266
2328
|
function transientReplayScopeKeys(input) {
|
|
2267
2329
|
const keys = [];
|
|
@@ -4002,7 +4064,8 @@ export function createChatWebApp(options = {}) {
|
|
|
4002
4064
|
if (url.pathname === `${CHAT_WEB_API_PREFIX}/trace/timeline` && request.method === "GET") {
|
|
4003
4065
|
const startedAt = performance.now();
|
|
4004
4066
|
const webSession = await requireSession(request, context);
|
|
4005
|
-
const
|
|
4067
|
+
const timelineCursor = parseTimelineBeforeCursor(url);
|
|
4068
|
+
const beforeSequence = timelineCursor.kind === "event" ? timelineCursor.beforeSequence : undefined;
|
|
4006
4069
|
const limit = parsePositiveIntSearchParam(url, "limit", TRACE_V2_DEFAULT_TIMELINE_LIMIT, TRACE_V2_MAX_TIMELINE_LIMIT);
|
|
4007
4070
|
const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined);
|
|
4008
4071
|
state.sessionQuery.upsertSession(selectedSession);
|
|
@@ -4011,8 +4074,8 @@ export function createChatWebApp(options = {}) {
|
|
|
4011
4074
|
let metadataMs = 0;
|
|
4012
4075
|
const lastEventSequence = state.timelineQuery.getLatestEventSequence(selectedSession.id);
|
|
4013
4076
|
const latestStreamId = state.timelineQuery.getLatestStreamId({ piboSessionId: selectedSession.id });
|
|
4014
|
-
const liveSnapshots =
|
|
4015
|
-
const transcriptMetadata =
|
|
4077
|
+
const liveSnapshots = timelineCursor.kind === "tail" ? state.outputCompactor.snapshotsForSession(selectedSession.id) : [];
|
|
4078
|
+
const transcriptMetadata = timelineCursor.kind === "tail" || timelineCursor.kind === "transcript"
|
|
4016
4079
|
? await loadPiSessionMetadata(selectedSession, selectedSession.workspace ?? process.cwd())
|
|
4017
4080
|
: undefined;
|
|
4018
4081
|
const baseVersion = createFastTraceV2Version({
|
|
@@ -4026,7 +4089,7 @@ export function createChatWebApp(options = {}) {
|
|
|
4026
4089
|
});
|
|
4027
4090
|
const snapshotVersion = liveSnapshotVersion(liveSnapshots);
|
|
4028
4091
|
const version = snapshotVersion ? `${baseVersion}:live:${snapshotVersion}` : baseVersion;
|
|
4029
|
-
const pageCursorKey =
|
|
4092
|
+
const pageCursorKey = timelineCursor.kind === "tail" ? "tail" : `before:${timelineCursor.raw}`;
|
|
4030
4093
|
const cacheKey = traceCacheKey(selectedSession.id, `${baseVersion}:v2:limit:${limit}:${pageCursorKey}`);
|
|
4031
4094
|
const pageCacheKey = traceCacheKey(selectedSession.id, `${version}:v2-page:limit:${limit}:${pageCursorKey}`);
|
|
4032
4095
|
const cachedPage = state.traceTimelinePageCache.get(pageCacheKey);
|
|
@@ -4036,7 +4099,7 @@ export function createChatWebApp(options = {}) {
|
|
|
4036
4099
|
"x-pibo-trace-version": version,
|
|
4037
4100
|
"cache-control": "no-store",
|
|
4038
4101
|
};
|
|
4039
|
-
if (
|
|
4102
|
+
if (timelineCursor.kind === "tail" && requestMatchesVersion(request, version)) {
|
|
4040
4103
|
return new Response(null, { status: 304, headers: baseHeaders });
|
|
4041
4104
|
}
|
|
4042
4105
|
if (cachedPage) {
|
|
@@ -4055,26 +4118,79 @@ export function createChatWebApp(options = {}) {
|
|
|
4055
4118
|
let trace = cached;
|
|
4056
4119
|
let eventCount = 0;
|
|
4057
4120
|
if (!trace) {
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4121
|
+
if (timelineCursor.kind === "transcript") {
|
|
4122
|
+
const history = transcriptMetadata?.sessionPath
|
|
4123
|
+
? readTranscriptHistoryPage(transcriptMetadata.sessionPath, {
|
|
4124
|
+
beforeByte: timelineCursor.beforeByte,
|
|
4125
|
+
beforeTimestamp: timelineCursor.beforeTimestamp,
|
|
4126
|
+
limit,
|
|
4127
|
+
})
|
|
4128
|
+
: { entries: [], hasOlder: false, scannedBytes: 0, startByte: 0, endByte: 0 };
|
|
4129
|
+
trace = await buildTraceView({
|
|
4130
|
+
session: selectedSession,
|
|
4131
|
+
sessions: ownedSessions,
|
|
4132
|
+
events: [],
|
|
4133
|
+
status: indexedSession?.status,
|
|
4134
|
+
metadata: transcriptMetadata ?? {},
|
|
4135
|
+
transcriptEntries: history.entries,
|
|
4136
|
+
transcriptOrderOffset: history.startByte,
|
|
4137
|
+
includeRawEvents: false,
|
|
4138
|
+
latestStreamId,
|
|
4139
|
+
});
|
|
4140
|
+
trace = {
|
|
4141
|
+
...trace,
|
|
4142
|
+
eventCount: lastEventSequence,
|
|
4143
|
+
eventLimit: limit,
|
|
4144
|
+
pageSize: limit,
|
|
4145
|
+
beforeCursor: timelineCursor.raw,
|
|
4146
|
+
nextBeforeCursor: history.hasOlder && history.nextBeforeByte !== undefined
|
|
4147
|
+
? encodeTranscriptTimelineCursor(history.nextBeforeByte, timelineCursor.beforeTimestamp)
|
|
4148
|
+
: undefined,
|
|
4149
|
+
hasOlderEvents: history.hasOlder,
|
|
4150
|
+
};
|
|
4151
|
+
}
|
|
4152
|
+
else {
|
|
4153
|
+
const events = state.timelineQuery.listTraceEvents({
|
|
4154
|
+
piboSessionId: selectedSession.id,
|
|
4155
|
+
limit,
|
|
4156
|
+
...(beforeSequence !== undefined ? { beforeSequence } : {}),
|
|
4157
|
+
});
|
|
4158
|
+
const transcriptEntries = timelineCursor.kind === "tail" && transcriptMetadata?.sessionPath
|
|
4159
|
+
? readTailEntries(transcriptMetadata.sessionPath)
|
|
4160
|
+
: [];
|
|
4161
|
+
eventCount = events.length;
|
|
4162
|
+
trace = await buildTraceView({
|
|
4163
|
+
session: selectedSession,
|
|
4164
|
+
sessions: ownedSessions,
|
|
4165
|
+
events,
|
|
4166
|
+
status: indexedSession?.status,
|
|
4167
|
+
metadata: transcriptMetadata ?? {},
|
|
4168
|
+
transcriptEntries,
|
|
4169
|
+
includeRawEvents: false,
|
|
4170
|
+
latestStreamId,
|
|
4171
|
+
});
|
|
4172
|
+
trace = annotateTracePage(trace, events, {
|
|
4173
|
+
lastEventSequence,
|
|
4174
|
+
pageSize: limit,
|
|
4175
|
+
beforeSequence,
|
|
4176
|
+
beforeCursor: timelineCursor.kind === "event" ? timelineCursor.raw : undefined,
|
|
4177
|
+
});
|
|
4178
|
+
if (trace.hasOlderEvents !== true && events.length > 0) {
|
|
4179
|
+
const cutoff = compactionCutoffTimestamp(events);
|
|
4180
|
+
if (cutoff) {
|
|
4181
|
+
const metadataStartedAt = performance.now();
|
|
4182
|
+
const metadata = transcriptMetadata ?? await loadPiSessionMetadata(selectedSession, selectedSession.workspace ?? process.cwd());
|
|
4183
|
+
metadataMs += performance.now() - metadataStartedAt;
|
|
4184
|
+
if (metadata.sessionPath && metadata.sessionSize && metadata.sessionSize > 0) {
|
|
4185
|
+
trace = {
|
|
4186
|
+
...trace,
|
|
4187
|
+
nextBeforeCursor: encodeTranscriptTimelineCursor(metadata.sessionSize, cutoff),
|
|
4188
|
+
hasOlderEvents: true,
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4078
4194
|
setTraceCache(state.traceCache, cacheKey, trace, TRACE_CACHE_MAX_ENTRIES);
|
|
4079
4195
|
}
|
|
4080
4196
|
trace = withLiveSnapshots(trace, liveSnapshots, {
|
|
@@ -4083,12 +4199,21 @@ export function createChatWebApp(options = {}) {
|
|
|
4083
4199
|
status: liveSnapshots.length ? "running" : indexedSession?.status,
|
|
4084
4200
|
});
|
|
4085
4201
|
trace = { ...trace, version };
|
|
4202
|
+
const transcriptTailCursor = timelineCursor.kind === "tail"
|
|
4203
|
+
? originTranscriptTailCursor({
|
|
4204
|
+
session: selectedSession,
|
|
4205
|
+
trace,
|
|
4206
|
+
metadata: transcriptMetadata,
|
|
4207
|
+
limit,
|
|
4208
|
+
})
|
|
4209
|
+
: undefined;
|
|
4086
4210
|
const page = traceTimelinePageFromView({
|
|
4087
4211
|
trace,
|
|
4088
4212
|
payloadStore: state.dataStore.payloads,
|
|
4089
4213
|
limit,
|
|
4090
4214
|
byteLimit: TRACE_V2_TIMELINE_HARD_BYTES,
|
|
4091
|
-
fromTail:
|
|
4215
|
+
fromTail: timelineCursor.kind === "tail",
|
|
4216
|
+
transcriptTailCursor,
|
|
4092
4217
|
});
|
|
4093
4218
|
setTraceTimelinePageCache(state.traceTimelinePageCache, pageCacheKey, page);
|
|
4094
4219
|
return responseJson(page, {
|
|
@@ -4119,7 +4244,10 @@ export function createChatWebApp(options = {}) {
|
|
|
4119
4244
|
}
|
|
4120
4245
|
if (url.pathname === `${CHAT_WEB_API_PREFIX}/trace/raw-events` && request.method === "GET") {
|
|
4121
4246
|
const webSession = await requireSession(request, context);
|
|
4122
|
-
const
|
|
4247
|
+
const rawCursor = parseTimelineBeforeCursor(url);
|
|
4248
|
+
if (rawCursor.kind === "transcript")
|
|
4249
|
+
throw new PiboWebHttpError("Raw event cursor must be tail or a positive sequence", 400);
|
|
4250
|
+
const beforeSequence = rawCursor.kind === "event" ? rawCursor.beforeSequence : undefined;
|
|
4123
4251
|
const limit = parsePositiveIntSearchParam(url, "limit", TRACE_V2_RAW_EVENTS_DEFAULT_LIMIT, TRACE_V2_RAW_EVENTS_MAX_LIMIT);
|
|
4124
4252
|
const selectedSession = resolveRequestedSession(state, context, webSession, defaultProfile, url.searchParams.get("piboSessionId") || undefined);
|
|
4125
4253
|
const events = state.timelineQuery.listTraceEvents({
|