@pasko70/pibo 3.4.4 → 3.5.1
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 +111 -25
- package/dist/agent-runtimes/codex-native/turn.js +27 -3
- 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 +40 -14
- 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 +37 -3
- 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 +149 -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 +645 -297
- package/dist/apps/chat-ui/assets/{dist-V06sfuZa.js → dist-B-auLrzD.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CH3SvpYV.js → dist-BA_dsINH.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-23lt_7qm.js → dist-eJZar_0-.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-Bf2KScPo.js → dist-wNNR2Bci.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-DsgL8w-W.js → dist-zcmEsIEp.js} +1 -1
- package/dist/apps/chat-ui/assets/index-DEkbN5Vo.js +229 -0
- package/dist/apps/chat-ui/assets/index-DZK6Tzil.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-DZgW1fCB.js +44 -0
- package/dist/apps/chat-vscode-web/index.html +1 -1
- package/dist/cli-session/localSessionSource.js +3 -2
- package/dist/compute/pool/seeds.js +25 -3
- package/dist/core/events.js +4 -0
- package/dist/core/output-render-sequence.js +65 -6
- 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 +178 -13
- package/dist/data/async-chat-reads.js +38 -0
- package/dist/data/async-chat-storage.js +96 -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 +167 -0
- package/dist/data/ingest-service.js +152 -18
- package/dist/data/message-command-store.js +290 -0
- package/dist/data/payload-store.js +92 -11
- package/dist/data/pibo-store.js +10 -8
- package/dist/data/schema.js +39 -4
- package/dist/data/session-store.js +3 -1
- package/dist/data/storage-backup.js +278 -0
- package/dist/data/storage-maintenance.js +344 -0
- package/dist/data/storage-verification-worker.js +25 -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 +207 -1
- package/dist/debug/message-queue.js +108 -0
- package/dist/debug/output-collision-repair.js +140 -0
- package/dist/debug/output-integrity.js +38 -2
- package/dist/debug/output-repair.js +1 -0
- package/dist/debug/storage-backup.js +41 -0
- package/dist/debug/storage-maintenance.js +78 -0
- package/dist/debug/telemetry-capture.js +66 -0
- package/dist/gateway/cli.js +71 -7
- package/dist/gateway/server.js +3 -0
- package/dist/providers/openai-gpt56.js +11 -6
- package/dist/reliability/store.js +119 -23
- package/dist/session-ui/terminalRows.js +54 -13
- package/dist/sessions/pibo-data-store.js +19 -14
- package/dist/shared/debug-features.js +4 -0
- package/dist/shared/model-inference-metrics.js +23 -0
- package/dist/shared/trace-event-projection.js +69 -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 +117 -12
- package/dist/web/http.js +135 -41
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-BOceJ0jM.css +0 -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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<meta name="theme-color" content="#101d22" />
|
|
7
7
|
<title>Pibo</title>
|
|
8
|
-
<script type="module" crossorigin src="/apps/chat-vscode/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/apps/chat-vscode/assets/index-DZgW1fCB.js"></script>
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-b18ZkEo0.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
@@ -641,7 +641,7 @@ export class LocalCliSessionSource {
|
|
|
641
641
|
if (delivery.persisted)
|
|
642
642
|
continue;
|
|
643
643
|
try {
|
|
644
|
-
this.ingestOutputEvent(session, delivery.event);
|
|
644
|
+
this.ingestOutputEvent(session, delivery.event, retryContext.attempt > 1 ? "durable-replay" : "live");
|
|
645
645
|
}
|
|
646
646
|
catch (error) {
|
|
647
647
|
errors.push(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -753,7 +753,7 @@ export class LocalCliSessionSource {
|
|
|
753
753
|
// Persistence is best-effort; live local rendering still proceeds through router events.
|
|
754
754
|
}
|
|
755
755
|
}
|
|
756
|
-
ingestOutputEvent(session, event) {
|
|
756
|
+
ingestOutputEvent(session, event, phase) {
|
|
757
757
|
if (!this.ingestService)
|
|
758
758
|
return;
|
|
759
759
|
this.ingestService.ingestOutputEvent({
|
|
@@ -762,6 +762,7 @@ export class LocalCliSessionSource {
|
|
|
762
762
|
actorId: session.profile,
|
|
763
763
|
event,
|
|
764
764
|
createdAt: this.now(),
|
|
765
|
+
persistenceProvenance: { producer: "local-cli", projection: "product-history", phase },
|
|
765
766
|
});
|
|
766
767
|
}
|
|
767
768
|
buildStatus(session) {
|
|
@@ -68,7 +68,7 @@ export async function prepareDeploymentSeed(input) {
|
|
|
68
68
|
await copySeedNonDatabaseFiles(input.config.seedSourceHome, homePath, input.mode, input.config.root);
|
|
69
69
|
const copiedDatabases = await backupSeedDatabases(input.config.seedSourceHome, homePath, input.mode);
|
|
70
70
|
if (input.config.seedSourcePiHome && existsSync(input.config.seedSourcePiHome)) {
|
|
71
|
-
await
|
|
71
|
+
await copyPiRuntimeSeed(input.config.seedSourcePiHome, piHomePath);
|
|
72
72
|
}
|
|
73
73
|
if (input.mode === "full" && input.config.seedSourceWorkspace && existsSync(input.config.seedSourceWorkspace)) {
|
|
74
74
|
await cp(input.config.seedSourceWorkspace, workspacePath, { recursive: true, force: true, preserveTimestamps: true });
|
|
@@ -160,11 +160,33 @@ async function backupSeedDatabases(sourceHome, destinationHome, mode) {
|
|
|
160
160
|
}
|
|
161
161
|
return copied.sort();
|
|
162
162
|
}
|
|
163
|
-
async function
|
|
163
|
+
async function copyPiRuntimeSeed(sourcePiHome, destinationPiHome) {
|
|
164
164
|
const sourceAgent = resolve(sourcePiHome, "agent");
|
|
165
165
|
const destinationAgent = resolve(destinationPiHome, "agent");
|
|
166
166
|
await mkdir(destinationAgent, { recursive: true, mode: 0o700 });
|
|
167
|
-
|
|
167
|
+
// OAuth refresh tokens belong to one credential store. Cloning them into isolated
|
|
168
|
+
// slots makes token rotation race with the source login and other slots.
|
|
169
|
+
const authPath = resolve(sourceAgent, "auth.json");
|
|
170
|
+
if (existsSync(authPath)) {
|
|
171
|
+
let parsed;
|
|
172
|
+
try {
|
|
173
|
+
parsed = JSON.parse(await readFile(authPath, "utf8"));
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
throw new Error("Cannot read the Pi credential seed.");
|
|
177
|
+
}
|
|
178
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
179
|
+
throw new Error("Invalid Pi credential seed.");
|
|
180
|
+
const apiKeys = Object.fromEntries(Object.entries(parsed).flatMap(([provider, value]) => {
|
|
181
|
+
if (!value || typeof value !== "object" || value.type !== "api_key" || typeof value.key !== "string")
|
|
182
|
+
return [];
|
|
183
|
+
return [[provider, { type: "api_key", key: value.key }]];
|
|
184
|
+
}));
|
|
185
|
+
if (Object.keys(apiKeys).length) {
|
|
186
|
+
await writeFile(resolve(destinationAgent, "auth.json"), `${JSON.stringify(apiKeys)}\n`, { mode: 0o600 });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const name of ["models-store.json"]) {
|
|
168
190
|
const source = resolve(sourceAgent, name);
|
|
169
191
|
if (!existsSync(source))
|
|
170
192
|
continue;
|
package/dist/core/events.js
CHANGED
|
@@ -4,3 +4,7 @@ export class PiboSteeringUnavailableError extends Error {
|
|
|
4
4
|
this.name = "PiboSteeringUnavailableError";
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
+
/** Only queue-clear actions can carry an already-completed ingress count. */
|
|
8
|
+
export function previouslyClearedMessages(event) {
|
|
9
|
+
return event.action === "clear_queue" && Number.isSafeInteger(event.clearedBeforeRuntime) && event.clearedBeforeRuntime > 0 ? event.clearedBeforeRuntime : 0;
|
|
10
|
+
}
|
|
@@ -109,9 +109,10 @@ export class OutputRenderSequencer {
|
|
|
109
109
|
const suppliedPart = transition.suppliedIndex === undefined
|
|
110
110
|
? undefined
|
|
111
111
|
: parts.find((part) => part.index === transition.suppliedIndex);
|
|
112
|
-
if (transition.suppliedIndex !== undefined && (transition.canonicalIndex
|
|
112
|
+
if (transition.suppliedIndex !== undefined && (transition.canonicalIndex
|
|
113
|
+
|| (suppliedPart && (!suppliedPart.closed || suppliedPart.identityFingerprint === transition.identityFingerprint)))) {
|
|
113
114
|
this.highWaterStore?.observeOutputPart?.({ ...transition, index: transition.suppliedIndex });
|
|
114
|
-
this.recordOutputPart(parts, transition.suppliedIndex, transition.terminal);
|
|
115
|
+
this.recordOutputPart(parts, transition.suppliedIndex, transition.terminal, transition.identityFingerprint);
|
|
115
116
|
this.trimOutputParts(state);
|
|
116
117
|
return event;
|
|
117
118
|
}
|
|
@@ -122,18 +123,20 @@ export class OutputRenderSequencer {
|
|
|
122
123
|
const index = latestOpen
|
|
123
124
|
? localIndex
|
|
124
125
|
: this.highWaterStore?.claimOrAttachOutputPart?.({ ...transition, proposedIndex: localIndex }) ?? localIndex;
|
|
125
|
-
this.recordOutputPart(parts, index, transition.terminal);
|
|
126
|
+
this.recordOutputPart(parts, index, transition.terminal, transition.identityFingerprint);
|
|
126
127
|
this.trimOutputParts(state);
|
|
127
128
|
return withOutputPartIndex(event, transition.kind, index);
|
|
128
129
|
}
|
|
129
|
-
recordOutputPart(parts, index, terminal) {
|
|
130
|
+
recordOutputPart(parts, index, terminal, identityFingerprint) {
|
|
130
131
|
let part = parts.find((candidate) => candidate.index === index);
|
|
131
132
|
if (!part) {
|
|
132
|
-
part = { index, closed: false };
|
|
133
|
+
part = { index, closed: false, identityFingerprint };
|
|
133
134
|
parts.push(part);
|
|
134
135
|
}
|
|
135
|
-
if (terminal)
|
|
136
|
+
if (terminal) {
|
|
136
137
|
part.closed = true;
|
|
138
|
+
part.identityFingerprint = identityFingerprint;
|
|
139
|
+
}
|
|
137
140
|
}
|
|
138
141
|
trimOutputParts(state) {
|
|
139
142
|
let total = 0;
|
|
@@ -380,11 +383,66 @@ function outputPartTransition(event, activeEventId) {
|
|
|
380
383
|
function validOutputPartIndex(value) {
|
|
381
384
|
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
382
385
|
}
|
|
386
|
+
export const OUTPUT_IDENTITY_FINGERPRINT_VERSION = 2;
|
|
383
387
|
export function outputIdentityFingerprint(event) {
|
|
388
|
+
return createHash("sha256").update(stableJson(outputIdentityPayload(event))).digest("hex");
|
|
389
|
+
}
|
|
390
|
+
/** Exact v1 algorithm retained only for comparing pre-v2 persisted fingerprints. */
|
|
391
|
+
export function legacyOutputIdentityFingerprint(event) {
|
|
384
392
|
const payload = { ...event };
|
|
385
393
|
delete payload.renderSequence;
|
|
394
|
+
delete payload.compactionStats;
|
|
386
395
|
return createHash("sha256").update(stableJson(payload)).digest("hex");
|
|
387
396
|
}
|
|
397
|
+
export function legacyOutputIdentityFingerprintCandidates(event) {
|
|
398
|
+
const variants = [{ ...event }];
|
|
399
|
+
const assistant = event.type === "assistant_delta" || event.type === "assistant_message";
|
|
400
|
+
const thinking = event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished";
|
|
401
|
+
const index = assistant ? event.assistantIndex ?? event.contentIndex : thinking ? event.thinkingIndex ?? event.contentIndex : undefined;
|
|
402
|
+
if (index !== undefined && (assistant || thinking)) {
|
|
403
|
+
for (const attribute of assistant ? ["assistantIndex", "contentIndex"] : ["thinkingIndex", "contentIndex"]) {
|
|
404
|
+
const variant = { ...event };
|
|
405
|
+
delete variant.assistantIndex;
|
|
406
|
+
delete variant.thinkingIndex;
|
|
407
|
+
delete variant.contentIndex;
|
|
408
|
+
variant[attribute] = index;
|
|
409
|
+
variants.push(variant);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
for (const variant of [...variants]) {
|
|
413
|
+
if ("provenance" in variant) {
|
|
414
|
+
const withoutProvenance = { ...variant };
|
|
415
|
+
delete withoutProvenance.provenance;
|
|
416
|
+
variants.push(withoutProvenance);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return [...new Set(variants.map((variant) => legacyOutputIdentityFingerprint(variant)))];
|
|
420
|
+
}
|
|
421
|
+
/** Redacted, bounded evidence for diagnosing a fingerprint mismatch without retaining values. */
|
|
422
|
+
export function outputIdentityFieldDigests(event) {
|
|
423
|
+
return Object.fromEntries(Object.entries(outputIdentityPayload(event))
|
|
424
|
+
.slice(0, 64)
|
|
425
|
+
.map(([field, value]) => [field, createHash("sha256").update(stableJson(value)).digest("hex").slice(0, 16)]));
|
|
426
|
+
}
|
|
427
|
+
function outputIdentityPayload(event) {
|
|
428
|
+
const payload = { ...event };
|
|
429
|
+
delete payload.renderSequence;
|
|
430
|
+
delete payload.compactionStats;
|
|
431
|
+
// Delivery provenance describes how equivalent output reached persistence; it
|
|
432
|
+
// is diagnostic metadata rather than semantic message content.
|
|
433
|
+
delete payload.provenance;
|
|
434
|
+
if (event.type === "assistant_delta" || event.type === "assistant_message") {
|
|
435
|
+
delete payload.assistantIndex;
|
|
436
|
+
delete payload.contentIndex;
|
|
437
|
+
payload.outputPartIndex = event.assistantIndex ?? event.contentIndex ?? 0;
|
|
438
|
+
}
|
|
439
|
+
else if (event.type === "thinking_started" || event.type === "thinking_delta" || event.type === "thinking_finished") {
|
|
440
|
+
delete payload.thinkingIndex;
|
|
441
|
+
delete payload.contentIndex;
|
|
442
|
+
payload.outputPartIndex = event.thinkingIndex ?? event.contentIndex ?? 0;
|
|
443
|
+
}
|
|
444
|
+
return payload;
|
|
445
|
+
}
|
|
388
446
|
export function outputPartFingerprint(event) {
|
|
389
447
|
const payload = { ...event };
|
|
390
448
|
delete payload.renderSequence;
|
|
@@ -393,6 +451,7 @@ export function outputPartFingerprint(event) {
|
|
|
393
451
|
delete payload.usageIndex;
|
|
394
452
|
delete payload.compactionIndex;
|
|
395
453
|
delete payload.contentIndex;
|
|
454
|
+
delete payload.compactionStats;
|
|
396
455
|
return createHash("sha256").update(stableJson(payload)).digest("hex");
|
|
397
456
|
}
|
|
398
457
|
function stableJson(value) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Pi's request boundary releases before tool work, so parents cannot pin slots while awaiting children. */
|
|
2
|
+
export function createProviderCapacityExtension(capacity, room, nested = false) {
|
|
3
|
+
return pi => {
|
|
4
|
+
let lease;
|
|
5
|
+
let removeAbort;
|
|
6
|
+
const release = () => { removeAbort?.(); removeAbort = undefined; lease?.release(); lease = undefined; };
|
|
7
|
+
pi.on("before_provider_request", async (_event, ctx) => {
|
|
8
|
+
// Retries replace a previous failed HTTP request, never overlap its reservation.
|
|
9
|
+
release();
|
|
10
|
+
const signal = ctx.signal;
|
|
11
|
+
try {
|
|
12
|
+
lease = await capacity.acquireProvider(ctx.model?.provider ?? "pi-unknown", room, signal, nested);
|
|
13
|
+
if (signal?.aborted) {
|
|
14
|
+
release();
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
signal?.addEventListener("abort", release, { once: true });
|
|
18
|
+
removeAbort = () => signal?.removeEventListener("abort", release);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
// Extension errors are observational in Pi. Abort explicitly so a rejected wait cannot send anyway.
|
|
22
|
+
ctx.abort();
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
pi.on("after_provider_response", event => { if (event.status >= 400)
|
|
27
|
+
release(); });
|
|
28
|
+
pi.on("message_end", event => { if (event.message.role === "assistant")
|
|
29
|
+
release(); });
|
|
30
|
+
pi.on("agent_end", release);
|
|
31
|
+
pi.on("session_shutdown", release);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -11,7 +11,8 @@ export class PiboProviderTelemetryRecorder {
|
|
|
11
11
|
}
|
|
12
12
|
recordRequestStart(payload, options = {}) {
|
|
13
13
|
const capturedOptions = { ...options, at: options.at ?? new Date().toISOString() };
|
|
14
|
-
|
|
14
|
+
const captured = providerPayloadSnapshot(payload);
|
|
15
|
+
return this.schedule({ kind: "start", payload: captured, options: capturedOptions }, () => this.recordRequestStartNow(captured, capturedOptions));
|
|
15
16
|
}
|
|
16
17
|
recordRequestStartNow(payload, options) {
|
|
17
18
|
if (!this.options.store)
|
|
@@ -79,7 +80,7 @@ export class PiboProviderTelemetryRecorder {
|
|
|
79
80
|
}
|
|
80
81
|
recordResponse(input) {
|
|
81
82
|
const captured = { status: input.status, at: input.at ?? new Date().toISOString() };
|
|
82
|
-
return this.schedule(() => this.recordResponseNow(captured));
|
|
83
|
+
return this.schedule({ kind: "response", input: captured }, () => this.recordResponseNow(captured));
|
|
83
84
|
}
|
|
84
85
|
recordResponseNow(input) {
|
|
85
86
|
if (!this.options.store)
|
|
@@ -141,7 +142,7 @@ export class PiboProviderTelemetryRecorder {
|
|
|
141
142
|
return undefined;
|
|
142
143
|
const captured = providerAssistantMessageSnapshot(message);
|
|
143
144
|
const capturedOptions = { ...options, at: options.at ?? new Date().toISOString() };
|
|
144
|
-
return this.schedule(() => this.recordMessageEndNow(captured, capturedOptions));
|
|
145
|
+
return this.schedule({ kind: "end", message: captured, options: capturedOptions }, () => this.recordMessageEndNow(captured, capturedOptions));
|
|
145
146
|
}
|
|
146
147
|
recordMessageEndNow(message, options) {
|
|
147
148
|
if (!this.options.store)
|
|
@@ -159,11 +160,25 @@ export class PiboProviderTelemetryRecorder {
|
|
|
159
160
|
return this.finishActiveProviderRequest(status, options.at ?? new Date().toISOString(), summary, errorMessage, errorDetails?.category ?? errorDetails?.errorClass);
|
|
160
161
|
}
|
|
161
162
|
recordShutdown(reason, at = new Date().toISOString()) {
|
|
162
|
-
return this.schedule(() => this.finishActiveProviderRequest("aborted", at, reason, undefined, "runtime_abort"));
|
|
163
|
+
return this.schedule({ kind: "shutdown", at, reason }, () => this.finishActiveProviderRequest("aborted", at, reason, undefined, "runtime_abort"));
|
|
163
164
|
}
|
|
164
|
-
|
|
165
|
+
executeTelemetryCommand(command) {
|
|
166
|
+
switch (command.kind) {
|
|
167
|
+
case "start":
|
|
168
|
+
this.recordRequestStartNow(command.payload, command.options);
|
|
169
|
+
break;
|
|
170
|
+
case "response":
|
|
171
|
+
this.recordResponseNow(command.input);
|
|
172
|
+
break;
|
|
173
|
+
case "end":
|
|
174
|
+
this.recordMessageEndNow(command.message, command.options);
|
|
175
|
+
break;
|
|
176
|
+
case "shutdown": this.finishActiveProviderRequest("aborted", command.at, command.reason, undefined, "runtime_abort");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
schedule(command, write) {
|
|
165
180
|
if (this.options.writer) {
|
|
166
|
-
this.options.writer.
|
|
181
|
+
this.options.writer.record({ recorder: "provider", command, session: { ...this.options.session, metadata: { chatRoomId: this.options.session.metadata?.chatRoomId ?? null, rootSessionId: this.options.session.metadata?.rootSessionId ?? null } }, model: this.options.model }, write, this.options.onError);
|
|
167
182
|
return undefined;
|
|
168
183
|
}
|
|
169
184
|
return write();
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export function resolveRuntimeCapacityOptions(env = process.env) {
|
|
2
|
+
const read = (name, fallback) => {
|
|
3
|
+
if (!env[name]?.trim())
|
|
4
|
+
return fallback;
|
|
5
|
+
const n = Number(env[name]);
|
|
6
|
+
if (!Number.isSafeInteger(n) || n < 1)
|
|
7
|
+
throw new Error(`${name} must be a positive integer.`);
|
|
8
|
+
return n;
|
|
9
|
+
};
|
|
10
|
+
return {
|
|
11
|
+
coldStarts: read("PIBO_GATEWAY_MAX_COLD_STARTS", 2), providerTurns: read("PIBO_GATEWAY_MAX_PROVIDER_TURNS", 10),
|
|
12
|
+
providerTurnsPerRoom: read("PIBO_GATEWAY_MAX_PROVIDER_TURNS_PER_ROOM", 5), maxWaiting: read("PIBO_GATEWAY_MAX_RUNTIME_WAITERS", 64),
|
|
13
|
+
maxWaitMs: read("PIBO_GATEWAY_MAX_RUNTIME_WAIT_MS", 60_000), maxRuntimes: read("PIBO_GATEWAY_MAX_ACTIVE_RUNTIMES", 32),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Bounded room round-robin. Cancellation removes a waiter without consuming a slot. */
|
|
17
|
+
export class FairCapacityPool {
|
|
18
|
+
limit;
|
|
19
|
+
perRoom;
|
|
20
|
+
maxWaiting;
|
|
21
|
+
maxWaitMs;
|
|
22
|
+
reserved;
|
|
23
|
+
rooms = new Map();
|
|
24
|
+
activeRooms = new Map();
|
|
25
|
+
active = 0;
|
|
26
|
+
generalActive = 0;
|
|
27
|
+
generalRooms = new Map();
|
|
28
|
+
waiting = 0;
|
|
29
|
+
closed = false;
|
|
30
|
+
constructor(limit, perRoom, maxWaiting, maxWaitMs, reserved = 0) {
|
|
31
|
+
this.limit = limit;
|
|
32
|
+
this.perRoom = perRoom;
|
|
33
|
+
this.maxWaiting = maxWaiting;
|
|
34
|
+
this.maxWaitMs = maxWaitMs;
|
|
35
|
+
this.reserved = reserved;
|
|
36
|
+
if (!Number.isSafeInteger(reserved) || reserved < 0 || reserved >= limit)
|
|
37
|
+
throw new Error("Reserved capacity must leave at least one normal slot.");
|
|
38
|
+
for (const n of [limit, perRoom, maxWaiting, maxWaitMs])
|
|
39
|
+
if (!Number.isSafeInteger(n) || n < 1)
|
|
40
|
+
throw new Error("Capacity limits must be positive integers.");
|
|
41
|
+
}
|
|
42
|
+
acquire(room, signal, reserved = false) {
|
|
43
|
+
if (this.closed)
|
|
44
|
+
return Promise.reject(capacityError("Runtime capacity is closed."));
|
|
45
|
+
if (signal?.aborted)
|
|
46
|
+
return Promise.reject(signal.reason ?? capacityError("Capacity wait cancelled."));
|
|
47
|
+
if (this.waiting >= this.maxWaiting - (reserved ? 0 : Math.min(this.reserved, this.maxWaiting - 1)))
|
|
48
|
+
return Promise.reject(capacityError("Runtime capacity wait queue is full."));
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
let timer;
|
|
51
|
+
const cancel = () => {
|
|
52
|
+
const queue = this.rooms.get(room);
|
|
53
|
+
const index = queue?.indexOf(waiter) ?? -1;
|
|
54
|
+
if (index < 0)
|
|
55
|
+
return;
|
|
56
|
+
queue.splice(index, 1);
|
|
57
|
+
this.waiting--;
|
|
58
|
+
if (!queue.length)
|
|
59
|
+
this.rooms.delete(room);
|
|
60
|
+
waiter.cleanup();
|
|
61
|
+
reject(signal?.aborted ? signal.reason : capacityError("Runtime capacity wait deadline exceeded."));
|
|
62
|
+
this.drain();
|
|
63
|
+
};
|
|
64
|
+
const waiter = { room, reserved, queuedAt: Date.now(), resolve, reject, cleanup: () => { clearTimeout(timer); signal?.removeEventListener("abort", cancel); } };
|
|
65
|
+
const queue = this.rooms.get(room) ?? [];
|
|
66
|
+
queue.push(waiter);
|
|
67
|
+
this.rooms.set(room, queue);
|
|
68
|
+
this.waiting++;
|
|
69
|
+
timer = setTimeout(cancel, this.maxWaitMs);
|
|
70
|
+
timer.unref?.();
|
|
71
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
72
|
+
this.drain();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
snapshot() {
|
|
76
|
+
let oldest = 0;
|
|
77
|
+
for (const queue of this.rooms.values())
|
|
78
|
+
oldest = Math.max(oldest, Date.now() - queue[0].queuedAt);
|
|
79
|
+
return { active: this.active, waiting: this.waiting, limit: this.limit, perRoom: this.perRoom, reserved: this.reserved, maxWaiting: this.maxWaiting, maxWaitMs: this.maxWaitMs, oldestWaitMs: oldest };
|
|
80
|
+
}
|
|
81
|
+
close() {
|
|
82
|
+
this.closed = true;
|
|
83
|
+
for (const queue of this.rooms.values())
|
|
84
|
+
for (const waiter of queue) {
|
|
85
|
+
waiter.cleanup();
|
|
86
|
+
waiter.reject(capacityError("Runtime capacity is closed."));
|
|
87
|
+
}
|
|
88
|
+
this.rooms.clear();
|
|
89
|
+
this.waiting = 0;
|
|
90
|
+
}
|
|
91
|
+
drain() {
|
|
92
|
+
while (!this.closed && this.active < this.limit) {
|
|
93
|
+
const eligible = (waiter) => (this.activeRooms.get(waiter.room) ?? 0) < this.perRoom && (waiter.reserved ||
|
|
94
|
+
(this.generalActive < this.limit - this.reserved && (this.generalRooms.get(waiter.room) ?? 0) < this.perRoom - Math.min(this.reserved, 1, this.perRoom - 1)));
|
|
95
|
+
const entry = [...this.rooms].find(([, queue]) => queue.some(eligible));
|
|
96
|
+
if (!entry)
|
|
97
|
+
return;
|
|
98
|
+
const [room, queue] = entry;
|
|
99
|
+
const [waiter] = queue.splice(queue.findIndex(eligible), 1);
|
|
100
|
+
this.rooms.delete(room);
|
|
101
|
+
if (queue.length)
|
|
102
|
+
this.rooms.set(room, queue);
|
|
103
|
+
if (!waiter.reserved) {
|
|
104
|
+
this.generalActive++;
|
|
105
|
+
this.generalRooms.set(room, (this.generalRooms.get(room) ?? 0) + 1);
|
|
106
|
+
}
|
|
107
|
+
this.waiting--;
|
|
108
|
+
this.active++;
|
|
109
|
+
this.activeRooms.set(room, (this.activeRooms.get(room) ?? 0) + 1);
|
|
110
|
+
waiter.cleanup();
|
|
111
|
+
let released = false;
|
|
112
|
+
waiter.resolve({ release: () => {
|
|
113
|
+
if (released)
|
|
114
|
+
return;
|
|
115
|
+
released = true;
|
|
116
|
+
this.active--;
|
|
117
|
+
if (!waiter.reserved) {
|
|
118
|
+
this.generalActive--;
|
|
119
|
+
const general = this.generalRooms.get(room) - 1;
|
|
120
|
+
if (general)
|
|
121
|
+
this.generalRooms.set(room, general);
|
|
122
|
+
else
|
|
123
|
+
this.generalRooms.delete(room);
|
|
124
|
+
}
|
|
125
|
+
const count = this.activeRooms.get(room) - 1;
|
|
126
|
+
if (count)
|
|
127
|
+
this.activeRooms.set(room, count);
|
|
128
|
+
else
|
|
129
|
+
this.activeRooms.delete(room);
|
|
130
|
+
this.drain();
|
|
131
|
+
} });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export class RuntimeCapacity {
|
|
136
|
+
starts;
|
|
137
|
+
maxRuntimes;
|
|
138
|
+
providers = new Map();
|
|
139
|
+
closed = false;
|
|
140
|
+
options;
|
|
141
|
+
constructor(options = {}) {
|
|
142
|
+
this.options = { ...resolveRuntimeCapacityOptions(), ...options };
|
|
143
|
+
for (const n of Object.values(this.options))
|
|
144
|
+
if (!Number.isSafeInteger(n) || n < 1)
|
|
145
|
+
throw new Error("Runtime capacity limits must be positive integers.");
|
|
146
|
+
this.maxRuntimes = this.options.maxRuntimes;
|
|
147
|
+
this.starts = new FairCapacityPool(this.options.coldStarts, 1, this.options.maxWaiting, this.options.maxWaitMs);
|
|
148
|
+
}
|
|
149
|
+
async acquireProvider(provider, room, signal, nested = false) {
|
|
150
|
+
if (this.closed)
|
|
151
|
+
throw capacityError("Runtime capacity is closed.");
|
|
152
|
+
let pool = this.providers.get(provider);
|
|
153
|
+
if (!pool) {
|
|
154
|
+
if (this.providers.size >= 64)
|
|
155
|
+
throw capacityError("Active provider capacity registry is full.");
|
|
156
|
+
pool = new FairCapacityPool(this.options.providerTurns, this.options.providerTurnsPerRoom, this.options.maxWaiting, this.options.maxWaitMs, Math.min(2, this.options.providerTurns - 1));
|
|
157
|
+
this.providers.set(provider, pool);
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
const lease = await pool.acquire(room, signal, nested);
|
|
161
|
+
return { release: () => { lease.release(); this.removeIdleProvider(provider, pool); } };
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
this.removeIdleProvider(provider, pool);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
snapshot() { return { coldStarts: this.starts.snapshot(), maxRuntimes: this.maxRuntimes, providers: [...this.providers].map(([provider, pool]) => ({ provider, ...pool.snapshot() })) }; }
|
|
169
|
+
close() { this.closed = true; this.starts.close(); for (const pool of this.providers.values())
|
|
170
|
+
pool.close(); this.providers.clear(); }
|
|
171
|
+
removeIdleProvider(provider, pool) { const state = pool.snapshot(); if (!state.active && !state.waiting && this.providers.get(provider) === pool)
|
|
172
|
+
this.providers.delete(provider); }
|
|
173
|
+
}
|
|
174
|
+
function capacityError(message) { return Object.assign(new Error(message), { code: "runtime_capacity_unavailable" }); }
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { BestEffortTelemetryService, } from "../data/telemetry.js";
|
|
2
2
|
import { isTerminalProviderStatus } from "./provider-telemetry.js";
|
|
3
|
+
/** Diagnostic cache only: terminal events clear their keys; the cap also contains orphaned/incomplete histories. */
|
|
4
|
+
class TelemetryProgressMap extends Map {
|
|
5
|
+
set(key, value) {
|
|
6
|
+
if (!this.has(key) && this.size >= 1024)
|
|
7
|
+
this.delete(this.keys().next().value);
|
|
8
|
+
return super.set(key, value);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
3
11
|
const TERMINAL_TURN_STATUSES = new Set(["ok", "error", "aborted", "timeout"]);
|
|
4
12
|
const DEFAULT_PROGRESS_FLUSH_INTERVAL_MS = 1_000;
|
|
5
13
|
export class PiboRuntimeTelemetryRecorder {
|
|
@@ -9,10 +17,11 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
9
17
|
providerEventMode;
|
|
10
18
|
progressFlushIntervalMs;
|
|
11
19
|
writer;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
phaseSequences = new TelemetryProgressMap();
|
|
21
|
+
pendingProviderProgress = new TelemetryProgressMap();
|
|
22
|
+
providerRequestCache = new TelemetryProgressMap();
|
|
23
|
+
lastProviderFlushAtMs = new TelemetryProgressMap();
|
|
24
|
+
lastProgressWriteAtMs = new TelemetryProgressMap();
|
|
16
25
|
constructor(store, onError, options = {}) {
|
|
17
26
|
this.store = store;
|
|
18
27
|
this.onError = onError;
|
|
@@ -29,7 +38,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
29
38
|
return;
|
|
30
39
|
const captured = captureTelemetryContext(context);
|
|
31
40
|
const capturedEvent = telemetryOutputEventSnapshot(event);
|
|
32
|
-
this.schedule(() => this.recordOutputUnsafe(capturedEvent, captured));
|
|
41
|
+
this.schedule({ kind: "output", event: capturedEvent, context: captured }, () => this.recordOutputUnsafe(capturedEvent, captured));
|
|
33
42
|
}
|
|
34
43
|
recordPiEvent(piboSessionId, event, context = {}) {
|
|
35
44
|
if (!this.store)
|
|
@@ -38,20 +47,29 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
38
47
|
if (!summary)
|
|
39
48
|
return;
|
|
40
49
|
const captured = captureTelemetryContext(context);
|
|
41
|
-
this.schedule(() => this.recordPiEventSummaryUnsafe(piboSessionId, summary, captured));
|
|
50
|
+
this.schedule({ kind: "pi", piboSessionId, summary, context: captured }, () => this.recordPiEventSummaryUnsafe(piboSessionId, summary, captured));
|
|
42
51
|
}
|
|
43
52
|
recordMessagesInterrupted(messages, context = {}, reason = "message interrupted") {
|
|
44
53
|
if (!this.store)
|
|
45
54
|
return;
|
|
46
55
|
const captured = captureTelemetryContext(context);
|
|
47
56
|
const interrupted = messages.flatMap((message) => message.id ? [{ piboSessionId: message.piboSessionId, eventId: message.id }] : []);
|
|
48
|
-
this.schedule(() => {
|
|
57
|
+
this.schedule({ kind: "interrupted", messages: interrupted, context: captured, reason }, () => {
|
|
49
58
|
for (const message of interrupted) {
|
|
50
59
|
this.recordTurnTerminal(message, captured, "aborted", "abort", reason, "runtime_abort");
|
|
51
60
|
}
|
|
52
61
|
});
|
|
53
62
|
}
|
|
54
|
-
|
|
63
|
+
executeTelemetryCommand(command) {
|
|
64
|
+
if (command.kind === "output")
|
|
65
|
+
this.recordOutputUnsafe(command.event, command.context);
|
|
66
|
+
else if (command.kind === "pi")
|
|
67
|
+
this.recordPiEventSummaryUnsafe(command.piboSessionId, command.summary, command.context);
|
|
68
|
+
else
|
|
69
|
+
for (const message of command.messages)
|
|
70
|
+
this.recordTurnTerminal(message, command.context, "aborted", "abort", command.reason, "runtime_abort");
|
|
71
|
+
}
|
|
72
|
+
schedule(command, write) {
|
|
55
73
|
const guarded = () => {
|
|
56
74
|
try {
|
|
57
75
|
write();
|
|
@@ -61,7 +79,7 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
61
79
|
}
|
|
62
80
|
};
|
|
63
81
|
if (this.writer)
|
|
64
|
-
this.writer.
|
|
82
|
+
this.writer.record({ recorder: "runtime", command, providerEventMode: this.providerEventMode, progressFlushIntervalMs: this.progressFlushIntervalMs }, guarded, this.onError);
|
|
65
83
|
else
|
|
66
84
|
guarded();
|
|
67
85
|
}
|
|
@@ -581,6 +599,10 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
581
599
|
counters: options.counters,
|
|
582
600
|
summary,
|
|
583
601
|
});
|
|
602
|
+
if (phase && !existing) {
|
|
603
|
+
const key = phaseId(turn.turnId, phaseName);
|
|
604
|
+
this.phaseSequences.set(key, (this.phaseSequences.get(key) ?? 0) + 1);
|
|
605
|
+
}
|
|
584
606
|
if (options.updateTurn) {
|
|
585
607
|
this.telemetry.upsertTurn({
|
|
586
608
|
turnId: turn.turnId,
|
|
@@ -602,7 +624,8 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
602
624
|
}
|
|
603
625
|
nextPhaseId(turnId, phaseName) {
|
|
604
626
|
const base = phaseId(turnId, phaseName);
|
|
605
|
-
const count = this.store?.countPhasesForTurn(turnId, phaseName) ?? 0;
|
|
627
|
+
const count = this.phaseSequences.get(base) ?? this.store?.countPhasesForTurn(turnId, phaseName) ?? 0;
|
|
628
|
+
this.phaseSequences.set(base, count);
|
|
606
629
|
if (count === 0)
|
|
607
630
|
return base;
|
|
608
631
|
return `${base}:${count + 1}`;
|
|
@@ -718,6 +741,9 @@ export class PiboRuntimeTelemetryRecorder {
|
|
|
718
741
|
this.lastProviderFlushAtMs.delete(turnId);
|
|
719
742
|
}
|
|
720
743
|
clearTurnProgress(turnId) {
|
|
744
|
+
for (const key of this.phaseSequences.keys())
|
|
745
|
+
if (key.startsWith(`${turnId}:`))
|
|
746
|
+
this.phaseSequences.delete(key);
|
|
721
747
|
this.clearProviderProgress(turnId);
|
|
722
748
|
const prefix = `${turnId}:`;
|
|
723
749
|
for (const key of this.lastProgressWriteAtMs.keys()) {
|
|
@@ -1069,6 +1095,8 @@ function telemetryOutputEventSnapshot(event) {
|
|
|
1069
1095
|
return { ...event, result: event.isError ? safeErrorMessage(event.result) : undefined };
|
|
1070
1096
|
if (event.type === "execution_result")
|
|
1071
1097
|
return { ...event, result: undefined };
|
|
1098
|
+
if (event.type === "assistant_delta" || event.type === "thinking_delta" || event.type === "assistant_message" || event.type === "message_queued" || event.type === "message_started")
|
|
1099
|
+
return { ...event, text: "" };
|
|
1072
1100
|
return { ...event };
|
|
1073
1101
|
}
|
|
1074
1102
|
function captureTelemetryContext(context) {
|
|
@@ -1076,8 +1104,8 @@ function captureTelemetryContext(context) {
|
|
|
1076
1104
|
const atMs = context.atMs ?? (Number.isFinite(parsedAtMs) ? parsedAtMs : Date.now());
|
|
1077
1105
|
return {
|
|
1078
1106
|
...context,
|
|
1079
|
-
session: context.session ? { ...context.session, metadata: context.session.metadata ? {
|
|
1080
|
-
status: context.status ? { ...context.status, activeTools: [
|
|
1107
|
+
session: context.session ? { ...context.session, metadata: context.session.metadata ? { chatRoomId: context.session.metadata.chatRoomId, rootSessionId: context.session.metadata.rootSessionId } : undefined } : undefined,
|
|
1108
|
+
status: context.status ? { ...context.status, activeTools: [], enabledTools: [] } : undefined,
|
|
1081
1109
|
at: context.at ?? new Date(atMs).toISOString(),
|
|
1082
1110
|
atMs,
|
|
1083
1111
|
};
|