@pasko70/pibo 3.4.4 → 3.5.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/dist/agent-runtime/routed-session.js +37 -12
- package/dist/agent-runtimes/pi/adapter.js +1 -0
- package/dist/agent-runtimes/pi/routed-session.js +2 -2
- package/dist/apps/chat/bounded-event-stream.js +98 -0
- package/dist/apps/chat/chat-settings-routes.js +3 -3
- package/dist/apps/chat/data/chat-data-mappers.js +32 -13
- package/dist/apps/chat/data/event-command-service.js +30 -21
- package/dist/apps/chat/data/history-query-service.js +34 -25
- package/dist/apps/chat/data/read-state-service.js +13 -0
- package/dist/apps/chat/data/session-query-service.js +19 -11
- package/dist/apps/chat/data/timeline-query-service.js +19 -7
- package/dist/apps/chat/message-command-dispatcher.js +134 -0
- package/dist/apps/chat/output-compactor.js +9 -0
- package/dist/apps/chat/output-event-policy.js +9 -1
- package/dist/apps/chat/stream.js +21 -3
- package/dist/apps/chat/telemetry-retention-service.js +113 -8
- package/dist/apps/chat/trace-response-cache.js +46 -0
- package/dist/apps/chat/trace-v2.js +12 -6
- package/dist/apps/chat/trace.js +1 -1
- package/dist/apps/chat/web-app.js +608 -281
- package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-BG0n7zLd.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-D6TjFhAm.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-D79vyxSX.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-DFZ8cwh0.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-cOjokPrK.js} +1 -1
- package/dist/apps/chat-ui/assets/index-RMHUTJ62.js +229 -0
- package/dist/apps/chat-ui/assets/{index-BOceJ0jM.css → index-hEkrlRk-.css} +1 -1
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-xacbCyTx.js +44 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/compute/pool/seeds.js +25 -3
- package/dist/core/events.js +4 -0
- package/dist/core/output-render-sequence.js +2 -0
- package/dist/core/provider-capacity.js +33 -0
- package/dist/core/provider-telemetry.js +21 -6
- package/dist/core/runtime-capacity.js +174 -0
- package/dist/core/runtime-telemetry.js +40 -12
- package/dist/core/session-router.js +79 -2
- package/dist/data/async-chat-reads.js +38 -0
- package/dist/data/async-chat-storage.js +91 -0
- package/dist/data/async-telemetry-maintenance.js +9 -0
- package/dist/data/bounded-worker-client.js +256 -0
- package/dist/data/chat-read-projections.js +159 -0
- package/dist/data/chat-read-worker.js +73 -0
- package/dist/data/chat-storage-worker.js +146 -0
- package/dist/data/ingest-service.js +79 -14
- package/dist/data/message-command-store.js +148 -0
- package/dist/data/payload-store.js +92 -11
- package/dist/data/pibo-store.js +10 -8
- package/dist/data/schema.js +16 -2
- package/dist/data/session-store.js +3 -1
- package/dist/data/storage-backup.js +278 -0
- package/dist/data/telemetry-capture.js +188 -0
- package/dist/data/telemetry-command.js +3 -0
- package/dist/data/telemetry-maintenance-worker.js +40 -0
- package/dist/data/telemetry-maintenance.js +110 -0
- package/dist/data/telemetry-retention.js +16 -7
- package/dist/data/telemetry-worker.js +111 -0
- package/dist/data/telemetry-writer.js +150 -83
- package/dist/data/telemetry.js +5 -0
- package/dist/debug/index.js +52 -0
- package/dist/debug/storage-backup.js +33 -0
- package/dist/debug/telemetry-capture.js +66 -0
- package/dist/gateway/server.js +2 -0
- package/dist/providers/openai-gpt56.js +11 -6
- package/dist/session-ui/terminalRows.js +48 -6
- package/dist/sessions/pibo-data-store.js +1 -0
- package/dist/shared/debug-features.js +4 -0
- package/dist/shared/model-inference-metrics.js +23 -0
- package/dist/shared/trace-event-projection.js +59 -2
- package/dist/shared/trace-history.js +9 -0
- package/dist/shared/trace-live-reducer.js +1 -0
- package/dist/shared/trace-patch-nodes.js +19 -0
- package/dist/web/channel.js +3 -0
- package/dist/web/http.js +36 -3
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-BOemYq-V.js +0 -228
- package/dist/apps/chat-vscode-web/assets/index-CMwTB8o8.js +0 -43
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { boundedMessageBytes } from "../../../data/bounded-worker-client.js";
|
|
2
3
|
import { messageTurnTimingsFromEvents } from "../../../shared/trace-event-projection.js";
|
|
3
4
|
import { storedChatEventFromV2Row, storedPiboEventFromV2Row } from "./chat-data-mappers.js";
|
|
4
5
|
import { TRACE_RECONCILIATION_ENTRY_CAP, TRACE_RECONCILIATION_TIMING_CAP } from "../../../shared/trace-limits.js";
|
|
5
6
|
import { parseTraceToolNodeIdentity } from "../../../shared/trace-tool-identity.js";
|
|
6
7
|
export class ChatTimelineQueryService {
|
|
7
8
|
store;
|
|
8
|
-
|
|
9
|
+
hydrationBytes;
|
|
10
|
+
constructor(store, hydrationBytes = 1024 * 1024) {
|
|
9
11
|
this.store = store;
|
|
12
|
+
this.hydrationBytes = hydrationBytes;
|
|
10
13
|
}
|
|
11
14
|
listEvents(input = {}) {
|
|
12
15
|
const clauses = [];
|
|
@@ -25,8 +28,17 @@ export class ChatTimelineQueryService {
|
|
|
25
28
|
}
|
|
26
29
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
27
30
|
const limit = Math.max(1, Math.min(input.limit ?? 1000, 5000));
|
|
28
|
-
const rows = this.store.db.prepare(`SELECT * FROM event_log ${where} ORDER BY stream_id ASC LIMIT ?`).
|
|
29
|
-
|
|
31
|
+
const rows = this.store.db.prepare(`SELECT * FROM event_log ${where} ORDER BY stream_id ASC LIMIT ?`).iterate(...values, limit);
|
|
32
|
+
const events = [];
|
|
33
|
+
let remainingBytes = 4 * 1024 * 1024 - 16;
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
const event = storedChatEventFromV2Row(row, this.store.payloads, this.hydrationBytes);
|
|
36
|
+
// Stop hydration as soon as the page exceeds IPC capacity. Callers can retry a
|
|
37
|
+
// smaller page without first allocating every large body in the requested range.
|
|
38
|
+
remainingBytes -= boundedMessageBytes(event, remainingBytes - 32) + 32;
|
|
39
|
+
events.push(event);
|
|
40
|
+
}
|
|
41
|
+
return events;
|
|
30
42
|
}
|
|
31
43
|
listSessionEvents(piboSessionId, limit = 1000) {
|
|
32
44
|
return this.listTraceEvents({ piboSessionId, limit, includeLive: true });
|
|
@@ -39,7 +51,7 @@ export class ChatTimelineQueryService {
|
|
|
39
51
|
}
|
|
40
52
|
scanMessageTurnTimings(piboSessionId) {
|
|
41
53
|
const rows = this.store.db.prepare(`
|
|
42
|
-
SELECT * FROM event_log
|
|
54
|
+
SELECT * FROM event_log INDEXED BY idx_event_log_timing_sequence
|
|
43
55
|
WHERE session_id = ?
|
|
44
56
|
AND type IN ('message_queued', 'message_steered', 'message_started', 'message_finished', 'session_error', 'thinking_finished', 'assistant_message')
|
|
45
57
|
ORDER BY session_sequence ASC, stream_id ASC
|
|
@@ -47,7 +59,7 @@ export class ChatTimelineQueryService {
|
|
|
47
59
|
`).all(piboSessionId, TRACE_RECONCILIATION_TIMING_CAP + 1);
|
|
48
60
|
if (rows.length > TRACE_RECONCILIATION_TIMING_CAP)
|
|
49
61
|
return { timings: [], overflow: true };
|
|
50
|
-
const events = rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads)).filter((event) => event !== undefined);
|
|
62
|
+
const events = rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads, this.hydrationBytes)).filter((event) => event !== undefined);
|
|
51
63
|
return { timings: messageTurnTimingsFromEvents(events), overflow: false };
|
|
52
64
|
}
|
|
53
65
|
listTraceEvents(input) {
|
|
@@ -70,14 +82,14 @@ export class ChatTimelineQueryService {
|
|
|
70
82
|
}
|
|
71
83
|
const rows = this.store.db.prepare(`
|
|
72
84
|
SELECT * FROM (
|
|
73
|
-
SELECT * FROM event_log
|
|
85
|
+
SELECT * FROM event_log ${includeLive ? "" : "INDEXED BY idx_event_log_semantic_sequence"}
|
|
74
86
|
WHERE ${clauses.join(" AND ")}
|
|
75
87
|
ORDER BY session_sequence DESC, stream_id DESC
|
|
76
88
|
LIMIT ?
|
|
77
89
|
)
|
|
78
90
|
ORDER BY session_sequence ASC, stream_id ASC
|
|
79
91
|
`).all(...values, limit);
|
|
80
|
-
return rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads)).filter((event) => event !== undefined);
|
|
92
|
+
return rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads, this.hydrationBytes)).filter((event) => event !== undefined);
|
|
81
93
|
}
|
|
82
94
|
isPayloadAttachedToTraceNode(input) {
|
|
83
95
|
const nodeIdentity = parseTraceToolNodeIdentity(input.nodeId);
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { PiboSteeringUnavailableError } from "../../core/events.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
/** Owns bounded dispatches; durable claims, not this map, own accepted work. */
|
|
4
|
+
export class MessageCommandDispatcher {
|
|
5
|
+
storage;
|
|
6
|
+
context;
|
|
7
|
+
owner = `message-dispatch:${randomUUID()}`;
|
|
8
|
+
claims = new Map();
|
|
9
|
+
lastRenewed = new Map();
|
|
10
|
+
timer;
|
|
11
|
+
disposed = false;
|
|
12
|
+
pumping;
|
|
13
|
+
wakePending = false;
|
|
14
|
+
lastFailureReportAt = 0;
|
|
15
|
+
leaseMs = 30_000;
|
|
16
|
+
constructor(storage, context) {
|
|
17
|
+
this.storage = storage;
|
|
18
|
+
this.context = context;
|
|
19
|
+
this.wake();
|
|
20
|
+
}
|
|
21
|
+
wake() {
|
|
22
|
+
if (this.disposed)
|
|
23
|
+
return;
|
|
24
|
+
if (this.pumping) {
|
|
25
|
+
this.wakePending = true;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (this.timer)
|
|
29
|
+
clearTimeout(this.timer);
|
|
30
|
+
this.timer = undefined;
|
|
31
|
+
let nextPollMs = 50;
|
|
32
|
+
this.pumping = this.pump().catch(error => {
|
|
33
|
+
nextPollMs = 1000;
|
|
34
|
+
this.wakePending = false;
|
|
35
|
+
if (Date.now() - this.lastFailureReportAt >= 5000) {
|
|
36
|
+
this.lastFailureReportAt = Date.now();
|
|
37
|
+
console.warn("[pibo] durable message dispatcher unavailable", { code: error && typeof error === "object" && "code" in error ? String(error.code) : "dispatch_failed" });
|
|
38
|
+
}
|
|
39
|
+
}).finally(() => {
|
|
40
|
+
this.pumping = undefined;
|
|
41
|
+
if (!this.disposed) {
|
|
42
|
+
if (this.wakePending) {
|
|
43
|
+
this.wakePending = false;
|
|
44
|
+
queueMicrotask(() => this.wake());
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
this.timer = setTimeout(() => this.wake(), nextPollMs);
|
|
48
|
+
this.timer.unref?.();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
outputPersisted(event) {
|
|
54
|
+
if (event.type !== "message_finished" && event.type !== "session_error" && event.type !== "message_steered")
|
|
55
|
+
return;
|
|
56
|
+
if (!event.eventId)
|
|
57
|
+
return;
|
|
58
|
+
for (const [id, claim] of this.claims)
|
|
59
|
+
if (claim.sessionId === event.piboSessionId && claim.eventId === event.eventId)
|
|
60
|
+
this.forget(id);
|
|
61
|
+
this.wake();
|
|
62
|
+
}
|
|
63
|
+
forget(id) { this.claims.delete(id); this.lastRenewed.delete(id); }
|
|
64
|
+
async pump() {
|
|
65
|
+
for (const [id, claim] of this.claims) {
|
|
66
|
+
if (this.disposed)
|
|
67
|
+
return;
|
|
68
|
+
if (Date.now() - (this.lastRenewed.get(id) ?? 0) < this.leaseMs / 3)
|
|
69
|
+
continue;
|
|
70
|
+
if (!await this.storage.heartbeatCommand(id, this.owner, claim.token, this.leaseMs))
|
|
71
|
+
this.forget(id);
|
|
72
|
+
else
|
|
73
|
+
this.lastRenewed.set(id, Date.now());
|
|
74
|
+
}
|
|
75
|
+
while (!this.disposed && this.claims.size < 12) {
|
|
76
|
+
const claim = await this.storage.claimCommand(this.owner, this.leaseMs);
|
|
77
|
+
if (!claim)
|
|
78
|
+
break;
|
|
79
|
+
this.claims.set(claim.id, claim);
|
|
80
|
+
this.lastRenewed.set(claim.id, Date.now());
|
|
81
|
+
// A slow cold runtime must not serialize unrelated session admission or dispatch.
|
|
82
|
+
void this.dispatch(claim);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async dispatch(claim) {
|
|
86
|
+
try {
|
|
87
|
+
if (this.disposed)
|
|
88
|
+
return;
|
|
89
|
+
if (!this.context.getSession(claim.sessionId)) {
|
|
90
|
+
await this.storage.transitionCommand(claim.id, this.owner, claim.token, "failed", "Target session no longer exists.");
|
|
91
|
+
this.forget(claim.id);
|
|
92
|
+
this.wake();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!await this.storage.transitionCommand(claim.id, this.owner, claim.token, "initializing")) {
|
|
96
|
+
this.forget(claim.id);
|
|
97
|
+
this.wake();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (this.disposed)
|
|
101
|
+
return;
|
|
102
|
+
const output = await this.context.emit({ type: "message", piboSessionId: claim.sessionId, id: claim.eventId, text: claim.text, delivery: claim.delivery, source: "user" });
|
|
103
|
+
if (output.type === "session_error") {
|
|
104
|
+
await this.storage.transitionCommand(claim.id, this.owner, claim.token, "failed", "Runtime rejected the accepted message.");
|
|
105
|
+
this.forget(claim.id);
|
|
106
|
+
this.wake();
|
|
107
|
+
}
|
|
108
|
+
// Output ingest advances queued/running/terminal state. No late emit result may downgrade it.
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (!this.disposed) {
|
|
112
|
+
const cancelled = Boolean(error && typeof error === "object" && "code" in error && error.code === "runtime_start_cancelled");
|
|
113
|
+
const steering = error instanceof PiboSteeringUnavailableError;
|
|
114
|
+
const capacity = Boolean(error && typeof error === "object" && "code" in error && error.code === "runtime_capacity_unavailable");
|
|
115
|
+
try {
|
|
116
|
+
await this.storage.transitionCommand(claim.id, this.owner, claim.token, cancelled || steering || capacity ? "failed" : "interrupted", cancelled ? "Message cancelled before runtime dispatch." : capacity ? "Runtime capacity was unavailable before dispatch; the message did not run." : steering ? "Steering is unavailable; the message was not queued as a normal turn." : "Runtime dispatch outcome is unclear; inspect the session before retrying.");
|
|
117
|
+
}
|
|
118
|
+
catch { /* Lease expiry retains the uncertain outcome. */ }
|
|
119
|
+
this.forget(claim.id);
|
|
120
|
+
this.wake();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async dispose() {
|
|
125
|
+
this.disposed = true;
|
|
126
|
+
if (this.timer)
|
|
127
|
+
clearTimeout(this.timer);
|
|
128
|
+
this.timer = undefined;
|
|
129
|
+
await this.pumping;
|
|
130
|
+
this.claims.clear();
|
|
131
|
+
this.lastRenewed.clear();
|
|
132
|
+
// Do not release dispatched claims for replay: the router may still own side effects.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { assistantOutputKey, isLiveOnlyOutputEvent, thinkingOutputKey, toolOutputKey } from "./output-event-policy.js";
|
|
2
2
|
const MAX_TRACKED_COMPACTOR_SESSIONS = 1_024;
|
|
3
3
|
export class OutputCompactor {
|
|
4
|
+
liveVersion = 0;
|
|
5
|
+
liveVersions = new Map();
|
|
6
|
+
versionForSession(id) { return this.liveVersions.get(id) ?? this.liveVersion; }
|
|
4
7
|
assistantBuffers = new Map();
|
|
5
8
|
thinkingBuffers = new Map();
|
|
6
9
|
toolSnapshots = new Map();
|
|
@@ -100,6 +103,10 @@ export class OutputCompactor {
|
|
|
100
103
|
settled = true;
|
|
101
104
|
for (const mutate of mutations)
|
|
102
105
|
mutate();
|
|
106
|
+
this.liveVersions.delete(event.piboSessionId);
|
|
107
|
+
this.liveVersions.set(event.piboSessionId, ++this.liveVersion);
|
|
108
|
+
while (this.liveVersions.size > MAX_TRACKED_COMPACTOR_SESSIONS)
|
|
109
|
+
this.liveVersions.delete(this.liveVersions.keys().next().value);
|
|
103
110
|
this.touchSession(event.piboSessionId);
|
|
104
111
|
};
|
|
105
112
|
return {
|
|
@@ -148,8 +155,10 @@ export class OutputCompactor {
|
|
|
148
155
|
}
|
|
149
156
|
this.sessionBufferCounts.delete(piboSessionId);
|
|
150
157
|
this.sessionRecency.delete(piboSessionId);
|
|
158
|
+
this.liveVersions.delete(piboSessionId);
|
|
151
159
|
}
|
|
152
160
|
disposeAll() {
|
|
161
|
+
this.liveVersions.clear();
|
|
153
162
|
this.assistantBuffers.clear();
|
|
154
163
|
this.thinkingBuffers.clear();
|
|
155
164
|
this.toolSnapshots.clear();
|
|
@@ -84,7 +84,8 @@ export function isPiboOutputEvent(value) {
|
|
|
84
84
|
return typeof value.reason === "string"
|
|
85
85
|
&& typeof value.aborted === "boolean"
|
|
86
86
|
&& isOptionalNonNegativeSafeInteger(value.compactionIndex)
|
|
87
|
-
&& isOptionalString(value.errorMessage)
|
|
87
|
+
&& isOptionalString(value.errorMessage)
|
|
88
|
+
&& (value.compactionStats === undefined || isCompactionStats(value.compactionStats));
|
|
88
89
|
case "approval_requested":
|
|
89
90
|
return isApprovalRequest(value.request);
|
|
90
91
|
case "approval_resolved":
|
|
@@ -117,6 +118,13 @@ function isOptionalFiniteNumber(value) {
|
|
|
117
118
|
function isOptionalString(value) {
|
|
118
119
|
return value === undefined || typeof value === "string";
|
|
119
120
|
}
|
|
121
|
+
function isCompactionStats(value) {
|
|
122
|
+
if (!isRecord(value) || !isNonNegativeSafeInteger(value.toolCallCount))
|
|
123
|
+
return false;
|
|
124
|
+
if (!isOptionalFiniteNumber(value.maxToolOutputTokens) || !isOptionalFiniteNumber(value.compactionTokens))
|
|
125
|
+
return false;
|
|
126
|
+
return value.maxToolOutputTokenBasis === undefined || typeof value.maxToolOutputTokenBasis === "string";
|
|
127
|
+
}
|
|
120
128
|
function isOptionalNonNegativeSafeInteger(value) {
|
|
121
129
|
return value === undefined || isNonNegativeSafeInteger(value);
|
|
122
130
|
}
|
package/dist/apps/chat/stream.js
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
+
class BoundedStreamIdentitySet extends Set {
|
|
2
|
+
bytes = 0;
|
|
3
|
+
add(key) {
|
|
4
|
+
if (this.has(key))
|
|
5
|
+
return this;
|
|
6
|
+
const bytes = key.length * 2;
|
|
7
|
+
if (bytes > 256 * 1024)
|
|
8
|
+
return this;
|
|
9
|
+
super.add(key);
|
|
10
|
+
this.bytes += bytes;
|
|
11
|
+
while (this.size > 2048 || this.bytes > 256 * 1024)
|
|
12
|
+
this.delete(this.values().next().value);
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
delete(key) { if (!super.delete(key))
|
|
16
|
+
return false; this.bytes -= key.length * 2; return true; }
|
|
17
|
+
clear() { super.clear(); this.bytes = 0; }
|
|
18
|
+
}
|
|
1
19
|
export function createChatStreamState() {
|
|
2
20
|
return {
|
|
3
|
-
textMessageIds: new
|
|
4
|
-
reasoningMessageIds: new
|
|
5
|
-
toolCallIds: new
|
|
21
|
+
textMessageIds: new BoundedStreamIdentitySet(),
|
|
22
|
+
reasoningMessageIds: new BoundedStreamIdentitySet(),
|
|
23
|
+
toolCallIds: new BoundedStreamIdentitySet(),
|
|
6
24
|
transientFrameIndex: 0,
|
|
7
25
|
};
|
|
8
26
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AsyncTelemetryMaintenance } from "../../data/async-telemetry-maintenance.js";
|
|
2
|
+
import { TelemetryMaintenance } from "../../data/telemetry-maintenance.js";
|
|
1
3
|
import { telemetryRetentionCutoff, } from "../../core/telemetry-retention-settings.js";
|
|
2
4
|
export const TELEMETRY_RETENTION_CLASSES = [
|
|
3
5
|
"live",
|
|
@@ -7,14 +9,33 @@ export const TELEMETRY_RETENTION_CLASSES = [
|
|
|
7
9
|
"incident",
|
|
8
10
|
];
|
|
9
11
|
export const DEFAULT_TELEMETRY_RETENTION_MAINTENANCE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
12
|
+
/** Bounded recovery: back off a failing maintenance job, then leave it visible instead of respawning. */
|
|
13
|
+
export const TELEMETRY_RETENTION_MAINTENANCE_FAILURE_LIMIT = 8;
|
|
14
|
+
export const TELEMETRY_RETENTION_MAINTENANCE_MAX_RETRY_MS = 30_000;
|
|
15
|
+
export async function disposeTelemetryRetentionMaintenance(state) {
|
|
16
|
+
state.disposed = true;
|
|
17
|
+
if (state.timer)
|
|
18
|
+
clearTimeout(state.timer);
|
|
19
|
+
state.timer = undefined;
|
|
20
|
+
state.running = false;
|
|
21
|
+
await state.worker?.close();
|
|
22
|
+
state.worker = undefined;
|
|
23
|
+
}
|
|
10
24
|
export function pruneTelemetryOlderThan(input) {
|
|
11
25
|
const cutoff = telemetryRetentionCutoff(input.days, input.now);
|
|
12
|
-
const results =
|
|
26
|
+
const results = [];
|
|
27
|
+
for (const retentionClass of TELEMETRY_RETENTION_CLASSES) {
|
|
28
|
+
const result = input.dataStore.telemetry.prune({ retentionClass, before: cutoff, apply: input.apply });
|
|
29
|
+
results.push(result);
|
|
30
|
+
if (result.completed === false)
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
13
33
|
return {
|
|
14
34
|
cutoff,
|
|
15
35
|
days: input.days,
|
|
16
36
|
applied: input.apply === true,
|
|
17
37
|
results,
|
|
38
|
+
completed: results.every(result => result.completed !== false),
|
|
18
39
|
rowsDeleted: results.reduce((sum, result) => sum + result.rowsDeleted, 0),
|
|
19
40
|
bytesMatched: results.reduce((sum, result) => sum + result.bytesMatched, 0),
|
|
20
41
|
};
|
|
@@ -27,6 +48,8 @@ export function isTelemetryRetentionMaintenanceDue(input) {
|
|
|
27
48
|
return !(input.state.lastCheckedAt && now.getTime() - input.state.lastCheckedAt < intervalMs);
|
|
28
49
|
}
|
|
29
50
|
export function maybeRunTelemetryRetentionMaintenance(input) {
|
|
51
|
+
if (input.state.disposed)
|
|
52
|
+
return;
|
|
30
53
|
if (!input.settings.enabled)
|
|
31
54
|
return;
|
|
32
55
|
if (input.state.running)
|
|
@@ -39,19 +62,70 @@ export function maybeRunTelemetryRetentionMaintenance(input) {
|
|
|
39
62
|
input.state.lastCheckedAt = now.getTime();
|
|
40
63
|
return;
|
|
41
64
|
}
|
|
42
|
-
if (hasActiveRuntimeWork(input.context))
|
|
43
|
-
return;
|
|
44
65
|
input.state.lastCheckedAt = now.getTime();
|
|
45
66
|
input.state.running = true;
|
|
46
|
-
|
|
67
|
+
const cutoff = telemetryRetentionCutoff(input.settings.days, now);
|
|
68
|
+
const local = input.dataStore.path === ":memory:" ? new TelemetryMaintenance(input.dataStore.db) : undefined;
|
|
69
|
+
let consecutiveFailures = 0;
|
|
70
|
+
const run = async () => {
|
|
71
|
+
let failed = false;
|
|
47
72
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
73
|
+
if (input.state.disposed)
|
|
74
|
+
return;
|
|
75
|
+
if (local) {
|
|
76
|
+
if (!local.status() || local.status()?.status === "completed" || local.status()?.status === "cancelled")
|
|
77
|
+
local.start(cutoff);
|
|
78
|
+
local.step();
|
|
79
|
+
}
|
|
80
|
+
else if (!input.state.worker) {
|
|
81
|
+
input.state.worker = new AsyncTelemetryMaintenance(input.dataStore.path);
|
|
82
|
+
await input.state.worker.command("start", cutoff);
|
|
83
|
+
}
|
|
84
|
+
const status = local?.status() ?? await input.state.worker.command("status");
|
|
85
|
+
if (input.state.disposed)
|
|
86
|
+
return;
|
|
87
|
+
if (status?.status === "completed") {
|
|
88
|
+
input.onPruned?.(now.toISOString());
|
|
89
|
+
input.state.running = false;
|
|
90
|
+
await input.state.worker?.close();
|
|
91
|
+
input.state.worker = undefined;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (status?.status === "cancelled" || status?.status === "paused") {
|
|
95
|
+
input.state.running = false;
|
|
96
|
+
await input.state.worker?.close();
|
|
97
|
+
input.state.worker = undefined;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
failed = true;
|
|
103
|
+
input.state.failures = (input.state.failures ?? 0) + 1;
|
|
104
|
+
await input.state.worker?.close();
|
|
105
|
+
input.state.worker = undefined;
|
|
106
|
+
}
|
|
107
|
+
if (input.state.disposed)
|
|
108
|
+
return;
|
|
109
|
+
if (!failed) {
|
|
110
|
+
consecutiveFailures = 0;
|
|
111
|
+
input.state.timer = setTimeout(() => { input.state.timer = undefined; void run(); }, local ? 25 : 250);
|
|
112
|
+
input.state.timer.unref?.();
|
|
113
|
+
return;
|
|
50
114
|
}
|
|
51
|
-
|
|
115
|
+
// A persistent failure must not respawn a worker thread every 250 ms forever. Back off, then stop
|
|
116
|
+
// with the failure count visible so a later due check retries instead of burning one worker per attempt.
|
|
117
|
+
consecutiveFailures++;
|
|
118
|
+
if (consecutiveFailures >= TELEMETRY_RETENTION_MAINTENANCE_FAILURE_LIMIT) {
|
|
52
119
|
input.state.running = false;
|
|
120
|
+
input.state.timer = undefined;
|
|
121
|
+
return;
|
|
53
122
|
}
|
|
54
|
-
|
|
123
|
+
const backoffMs = Math.min(TELEMETRY_RETENTION_MAINTENANCE_MAX_RETRY_MS, 250 * 2 ** (consecutiveFailures - 1));
|
|
124
|
+
input.state.timer = setTimeout(() => { input.state.timer = undefined; void run(); }, backoffMs);
|
|
125
|
+
input.state.timer.unref?.();
|
|
126
|
+
};
|
|
127
|
+
input.state.timer = setTimeout(() => { input.state.timer = undefined; void run(); }, 0);
|
|
128
|
+
input.state.timer.unref?.();
|
|
55
129
|
}
|
|
56
130
|
export function isPersistentRetentionDue(lastPrunedAt, now, intervalMs) {
|
|
57
131
|
if (!lastPrunedAt)
|
|
@@ -67,3 +141,34 @@ export function hasActiveRuntimeWork(context) {
|
|
|
67
141
|
?? [];
|
|
68
142
|
return statuses.some((status) => Boolean(status?.processing || status?.streaming || (status?.queuedMessages ?? 0) > 0));
|
|
69
143
|
}
|
|
144
|
+
/** Explicit HTTP maintenance keeps SQL off the gateway and yields between every bounded batch. */
|
|
145
|
+
export async function pruneTelemetryOlderThanAsync(input) {
|
|
146
|
+
if (input.dataStore.path === ":memory:")
|
|
147
|
+
return pruneTelemetryOlderThan(input);
|
|
148
|
+
const cutoff = telemetryRetentionCutoff(input.days), worker = new AsyncTelemetryMaintenance(input.dataStore.path);
|
|
149
|
+
const abort = () => { void worker.close(); };
|
|
150
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
151
|
+
try {
|
|
152
|
+
if (input.signal?.aborted)
|
|
153
|
+
throw Error("Maintenance request cancelled");
|
|
154
|
+
if (!input.apply) {
|
|
155
|
+
const results = await worker.preview(cutoff);
|
|
156
|
+
return { cutoff, days: input.days, applied: false, results, rowsDeleted: 0, bytesMatched: results.reduce((n, r) => n + r.bytesMatched, 0), completed: true };
|
|
157
|
+
}
|
|
158
|
+
let status = await worker.command("start", cutoff);
|
|
159
|
+
if (status?.status === "paused")
|
|
160
|
+
status = await worker.command("resume");
|
|
161
|
+
const until = Date.now() + 30000;
|
|
162
|
+
while (status?.status === "running" && Date.now() < until) {
|
|
163
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
164
|
+
status = await worker.command("status");
|
|
165
|
+
}
|
|
166
|
+
if (status?.status === "running")
|
|
167
|
+
status = await worker.command("pause");
|
|
168
|
+
return { cutoff: status?.cutoff ?? cutoff, days: input.days, applied: true, results: [], rowsDeleted: status?.deleted ?? 0, bytesMatched: 0, completed: status?.status === "completed" };
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
input.signal?.removeEventListener("abort", abort);
|
|
172
|
+
await worker.close();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Exact encoded responses; bounded independently from reconstructed trace caches. */
|
|
2
|
+
export class TraceResponseCache {
|
|
3
|
+
maximumBytes;
|
|
4
|
+
maximumEntries;
|
|
5
|
+
entries = new Map();
|
|
6
|
+
bytes = 0;
|
|
7
|
+
constructor(maximumBytes = 8 * 1024 * 1024, maximumEntries = 32) {
|
|
8
|
+
this.maximumBytes = maximumBytes;
|
|
9
|
+
this.maximumEntries = maximumEntries;
|
|
10
|
+
}
|
|
11
|
+
get(key, request) {
|
|
12
|
+
const entry = this.entries.get(key);
|
|
13
|
+
if (!entry)
|
|
14
|
+
return;
|
|
15
|
+
this.entries.delete(key);
|
|
16
|
+
this.entries.set(key, entry);
|
|
17
|
+
const headers = new Headers(entry.headers);
|
|
18
|
+
headers.set("server-timing", 'trace_cache;desc="watermark-hit"');
|
|
19
|
+
const etag = headers.get("etag");
|
|
20
|
+
const requested = request.headers.get("if-none-match");
|
|
21
|
+
if (etag && requested?.split(',').some(v => v.trim() === '*' || v.trim() === etag || v.trim() === 'W/' + etag))
|
|
22
|
+
return new Response(null, { status: 304, headers });
|
|
23
|
+
return new Response(entry.body.slice(), { status: entry.status, headers });
|
|
24
|
+
}
|
|
25
|
+
async set(key, response) {
|
|
26
|
+
if (response.status !== 200 || !response.headers.get("content-type")?.includes("json"))
|
|
27
|
+
return;
|
|
28
|
+
const body = new Uint8Array(await response.clone().arrayBuffer());
|
|
29
|
+
const bytes = body.byteLength + Buffer.byteLength(key);
|
|
30
|
+
if (bytes > Math.min(this.maximumBytes, 1024 * 1024))
|
|
31
|
+
return;
|
|
32
|
+
const previous = this.entries.get(key);
|
|
33
|
+
if (previous)
|
|
34
|
+
this.bytes -= previous.bytes;
|
|
35
|
+
this.entries.delete(key);
|
|
36
|
+
this.entries.set(key, { body, headers: [...response.headers.entries()], status: response.status, bytes });
|
|
37
|
+
this.bytes += bytes;
|
|
38
|
+
while (this.bytes > this.maximumBytes || this.entries.size > this.maximumEntries) {
|
|
39
|
+
const oldest = this.entries.keys().next().value;
|
|
40
|
+
this.bytes -= this.entries.get(oldest).bytes;
|
|
41
|
+
this.entries.delete(oldest);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
clear() { this.entries.clear(); this.bytes = 0; }
|
|
45
|
+
status() { return { entries: this.entries.size, bytes: this.bytes, maximumBytes: this.maximumBytes, maximumEntries: this.maximumEntries }; }
|
|
46
|
+
}
|
|
@@ -148,17 +148,21 @@ export function looksLikeImagePayloadPreview(value) {
|
|
|
148
148
|
return /"type"\s*:\s*"(?:image|input_image|output_image|image_url)"/i.test(value)
|
|
149
149
|
|| /"mime(?:Type|_type)"\s*:\s*"image\//i.test(value);
|
|
150
150
|
}
|
|
151
|
-
export function readTracePayloadChunk(input) {
|
|
151
|
+
export async function readTracePayloadChunk(input) {
|
|
152
152
|
const parsed = parseTracePayloadRef(input.ref);
|
|
153
153
|
if (!parsed)
|
|
154
154
|
return undefined;
|
|
155
155
|
const payload = input.payloadStore.getPayload(parsed.payloadId);
|
|
156
156
|
if (!payload)
|
|
157
157
|
return undefined;
|
|
158
|
-
const
|
|
159
|
-
const offset = Math.max(0, Math.min(input.offset, bytes.byteLength));
|
|
158
|
+
const offset = Math.max(0, Math.min(input.offset, payload.byteSize));
|
|
160
159
|
const limit = Math.max(1, Math.min(input.limit, TRACE_V2_PAYLOAD_MAX_LIMIT_BYTES));
|
|
161
|
-
const
|
|
160
|
+
const bytes = await input.payloadStore.readPayloadRange(parsed.payloadId, offset, limit + 3);
|
|
161
|
+
let length = Math.min(limit, bytes.length);
|
|
162
|
+
// Include a complete UTF-8 code point at the boundary; cursors remain byte offsets.
|
|
163
|
+
while (length < bytes.length && (bytes[length] & 0xc0) === 0x80)
|
|
164
|
+
length++;
|
|
165
|
+
const chunk = bytes.subarray(0, length);
|
|
162
166
|
const preview = payload.previewText ?? "";
|
|
163
167
|
const traceRef = {
|
|
164
168
|
ref: input.ref,
|
|
@@ -168,7 +172,7 @@ export function readTracePayloadChunk(input) {
|
|
|
168
172
|
truncatedPreview: Buffer.byteLength(preview, "utf8") < payload.byteSize,
|
|
169
173
|
hash: payload.sha256,
|
|
170
174
|
};
|
|
171
|
-
const nextOffset = offset + chunk.byteLength <
|
|
175
|
+
const nextOffset = offset + chunk.byteLength < payload.byteSize ? offset + chunk.byteLength : undefined;
|
|
172
176
|
return {
|
|
173
177
|
ref: traceRef,
|
|
174
178
|
offset,
|
|
@@ -248,6 +252,8 @@ function compactTraceNode(node, payloadStore, piboSessionId, depth) {
|
|
|
248
252
|
completedAt: node.completedAt,
|
|
249
253
|
durationMs: node.durationMs,
|
|
250
254
|
toolMetrics: node.toolMetrics,
|
|
255
|
+
modelInferences: node.modelInferences,
|
|
256
|
+
compactionStats: node.compactionStats,
|
|
251
257
|
orderKey: node.orderKey,
|
|
252
258
|
depth,
|
|
253
259
|
hasChildren: node.children.length > 0,
|
|
@@ -268,7 +274,7 @@ function compactTraceNode(node, payloadStore, piboSessionId, depth) {
|
|
|
268
274
|
});
|
|
269
275
|
}
|
|
270
276
|
function inlinePayloadForNodeValue(node, kind, value) {
|
|
271
|
-
return inlinePayloadForValue(value, inlinePayloadByteLimit(node, kind));
|
|
277
|
+
return node.payloadRefs?.[kind] ? undefined : inlinePayloadForValue(value, inlinePayloadByteLimit(node, kind));
|
|
272
278
|
}
|
|
273
279
|
function inlinePayloadByteLimit(node, kind) {
|
|
274
280
|
if ((node.type === "user.message" || node.type === "assistant.message" || node.type === "model.reasoning") && kind !== "error") {
|
package/dist/apps/chat/trace.js
CHANGED
|
@@ -5,7 +5,7 @@ import { isBuiltInHistoryReconciliationProof } from "../../agent-runtimes/histor
|
|
|
5
5
|
import { buildTraceViewFromEvents, traceNodesFromHistoryEntries } from "../../shared/trace-engine.js";
|
|
6
6
|
import { chatWebSessionSidebarOrder, isChatWebSessionArchived, isChatWebSessionPinned } from "./session-metadata.js";
|
|
7
7
|
import { workflowSessionKindFromMetadata } from "../../sessions/workflow-session-kind.js";
|
|
8
|
-
export const TRACE_PROJECTION_VERSION = "runtime-history-v2-
|
|
8
|
+
export const TRACE_PROJECTION_VERSION = "runtime-history-v2-debug-metrics";
|
|
9
9
|
export { compareTraceNodes, sortTraceNodes, nestTraceNodes, flattenTraceNodes, mapTraceNodesById, buildTraceViewFromEvents, traceNodesFromHistoryEntries, } from "../../shared/trace-engine.js";
|
|
10
10
|
export const TRACE_TRANSCRIPT_TAIL_MAX_BYTES = PI_HISTORY_TAIL_MAX_BYTES;
|
|
11
11
|
export const TRACE_TRANSCRIPT_HISTORY_PAGE_MAX_BYTES = PI_HISTORY_PAGE_MAX_BYTES;
|