@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,3 +1,4 @@
|
|
|
1
|
+
import { previouslyClearedMessages } from "../core/events.js";
|
|
1
2
|
import { PiboSteeringUnavailableError, } from "../core/events.js";
|
|
2
3
|
import { PIBO_PROVIDER_RECOVERY_PROMPT, isPiboProviderFallbackError } from "../core/provider-recovery.js";
|
|
3
4
|
import { normalizeSessionErrorDetails, runtimeSessionErrorDetails } from "../core/session-errors.js";
|
|
@@ -166,8 +167,14 @@ export class RuntimeRoutedSession {
|
|
|
166
167
|
if (this.sessionIdentityOperationInFlight && !this.forkCandidatesRequest) {
|
|
167
168
|
throw new Error("Pibo session cannot accept messages while a session identity operation is in progress.");
|
|
168
169
|
}
|
|
170
|
+
const messages = this.queue.filter(item => item.kind === "message");
|
|
171
|
+
const bytes = Buffer.byteLength(event.text);
|
|
172
|
+
if (bytes > 1024 * 1024 || messages.length >= 64 || messages.reduce((total, item) => total + Buffer.byteLength(item.event.text), bytes) > 4 * 1024 * 1024
|
|
173
|
+
|| messages.some(item => Date.now() - item.acceptedAt >= 10 * 60 * 1000)) {
|
|
174
|
+
throw Object.assign(new Error("Session runtime queue count, byte or wait-age capacity reached."), { code: "runtime_capacity_unavailable" });
|
|
175
|
+
}
|
|
169
176
|
onAccepted();
|
|
170
|
-
this.queue.push({ kind: "message", event });
|
|
177
|
+
this.queue.push({ kind: "message", event, acceptedAt: Date.now() });
|
|
171
178
|
const output = {
|
|
172
179
|
type: "message_queued",
|
|
173
180
|
piboSessionId: this.piboSessionId,
|
|
@@ -593,6 +600,7 @@ export class RuntimeRoutedSession {
|
|
|
593
600
|
if (inFlight?.event.id === eventId) {
|
|
594
601
|
if (!inFlight.cancellation) {
|
|
595
602
|
inFlight.cancelled = true;
|
|
603
|
+
inFlight.capacityAbort.abort(new Error("Message cancelled while waiting for capacity."));
|
|
596
604
|
this.notifyMessagesInterrupted([inFlight.event], "message cancelled");
|
|
597
605
|
const active = this.activeMessage?.id === eventId;
|
|
598
606
|
inFlight.cancellation = (async () => {
|
|
@@ -710,6 +718,7 @@ export class RuntimeRoutedSession {
|
|
|
710
718
|
result: event.result,
|
|
711
719
|
aborted: event.aborted,
|
|
712
720
|
errorMessage: event.errorMessage,
|
|
721
|
+
compactionStats: event.compactionStats,
|
|
713
722
|
}));
|
|
714
723
|
return;
|
|
715
724
|
case "approval_requested":
|
|
@@ -815,11 +824,29 @@ export class RuntimeRoutedSession {
|
|
|
815
824
|
this.notifyState();
|
|
816
825
|
}
|
|
817
826
|
}
|
|
827
|
+
async promptWithinCapacity(event, inFlight, input) {
|
|
828
|
+
const provider = this.runtimeSession.getStatus().activeModel?.provider ?? this.runtimeSession.runtimeInstanceId;
|
|
829
|
+
const lease = await this.options.acquireProviderCapacity?.(provider, inFlight.capacityAbort.signal);
|
|
830
|
+
try {
|
|
831
|
+
if (this.disposed || inFlight.cancelled)
|
|
832
|
+
return;
|
|
833
|
+
if (!inFlight.started) {
|
|
834
|
+
inFlight.started = true;
|
|
835
|
+
this.emit({ type: "message_started", piboSessionId: this.piboSessionId, eventId: event.id, text: event.text, source: event.source, provenance: event.provenance });
|
|
836
|
+
}
|
|
837
|
+
if (this.disposed || inFlight.cancelled)
|
|
838
|
+
return;
|
|
839
|
+
await this.runtimeSession.prompt(input);
|
|
840
|
+
}
|
|
841
|
+
finally {
|
|
842
|
+
lease?.release();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
818
845
|
async promptWithModelFallbacks(event, inFlight) {
|
|
819
846
|
const canSetModel = Boolean(this.runtimeSession.controls?.setModel);
|
|
820
847
|
const fallbackModels = this.modelFallbacks.filter((model) => !sameModel(model, this.primaryModel));
|
|
821
848
|
if (!canSetModel || fallbackModels.length === 0) {
|
|
822
|
-
await this.
|
|
849
|
+
await this.promptWithinCapacity(event, inFlight, {
|
|
823
850
|
text: event.text,
|
|
824
851
|
source: promptSource(event.source),
|
|
825
852
|
capabilityScope: event.capabilityScope,
|
|
@@ -845,7 +872,7 @@ export class RuntimeRoutedSession {
|
|
|
845
872
|
this.pendingProviderFailure = undefined;
|
|
846
873
|
let promptError;
|
|
847
874
|
try {
|
|
848
|
-
await this.
|
|
875
|
+
await this.promptWithinCapacity(event, inFlight, {
|
|
849
876
|
text: attempt === 0 ? event.text : PIBO_PROVIDER_RECOVERY_PROMPT,
|
|
850
877
|
source: attempt === 0 ? promptSource(event.source) : "rpc",
|
|
851
878
|
capabilityScope: event.capabilityScope,
|
|
@@ -907,14 +934,6 @@ export class RuntimeRoutedSession {
|
|
|
907
934
|
this.activeMessageFailed = false;
|
|
908
935
|
this.beginRunReminderTurnGuard(event);
|
|
909
936
|
this.resetContentIndices();
|
|
910
|
-
this.emit({
|
|
911
|
-
type: "message_started",
|
|
912
|
-
piboSessionId: this.piboSessionId,
|
|
913
|
-
eventId: event.id,
|
|
914
|
-
text: event.text,
|
|
915
|
-
source: event.source,
|
|
916
|
-
provenance: event.provenance,
|
|
917
|
-
});
|
|
918
937
|
if (inFlight.cancelled)
|
|
919
938
|
return;
|
|
920
939
|
await this.promptWithModelFallbacks(event, inFlight);
|
|
@@ -963,6 +982,7 @@ export class RuntimeRoutedSession {
|
|
|
963
982
|
const inFlight = {
|
|
964
983
|
event,
|
|
965
984
|
cancelled: false,
|
|
985
|
+
capacityAbort: new AbortController(),
|
|
966
986
|
settled: new Promise((resolve) => { resolveSettled = resolve; }),
|
|
967
987
|
resolveSettled: () => { resolveSettled?.(); },
|
|
968
988
|
};
|
|
@@ -1129,8 +1149,12 @@ export class RuntimeRoutedSession {
|
|
|
1129
1149
|
? await this.options.logoutRuntimeAuth(input)
|
|
1130
1150
|
: await this.pluginRegistry.logoutAgentRuntimeAuth(this.runtimeSession.runtimeInstanceId, input),
|
|
1131
1151
|
getProviderUsage: () => this.getActionProviderUsage(),
|
|
1132
|
-
clearQueue: () => this.clearQueue(),
|
|
1152
|
+
clearQueue: () => this.clearQueue() + previouslyClearedMessages(event),
|
|
1133
1153
|
abort: async () => {
|
|
1154
|
+
if (this.inFlightMessage && !this.inFlightMessage.started) {
|
|
1155
|
+
await this.cancelMessage(this.inFlightMessage.event.id);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1134
1158
|
if (this.activeMessage)
|
|
1135
1159
|
this.notifyMessagesInterrupted([this.activeMessage], "abort requested");
|
|
1136
1160
|
await this.runtimeSession.abort();
|
|
@@ -1187,6 +1211,7 @@ export class RuntimeRoutedSession {
|
|
|
1187
1211
|
if (this.disposed)
|
|
1188
1212
|
return false;
|
|
1189
1213
|
const activeMessage = this.activeMessage;
|
|
1214
|
+
this.inFlightMessage?.capacityAbort.abort(new Error(reason));
|
|
1190
1215
|
this.notifyMessagesInterrupted(this.activeAndQueuedMessages(), reason);
|
|
1191
1216
|
if (activeMessage) {
|
|
1192
1217
|
const error = `Session disposed while a message was active: ${reason}`;
|
|
@@ -253,6 +253,7 @@ export function semanticEventFromPibo(event) {
|
|
|
253
253
|
result: event.result,
|
|
254
254
|
aborted: event.aborted,
|
|
255
255
|
errorMessage: event.errorMessage,
|
|
256
|
+
compactionStats: event.compactionStats,
|
|
256
257
|
};
|
|
257
258
|
case "session_error":
|
|
258
259
|
return { type: "error", message: event.error, details: event.errorDetails };
|
|
@@ -863,8 +863,6 @@ export class RoutedSession {
|
|
|
863
863
|
if (assistantMessageEnded && !assistantMessageFailed)
|
|
864
864
|
this.flushPendingProviderWebSearchFinishes();
|
|
865
865
|
const usageEvent = assistantMessageEnded ? normalizeAssistantUsageEvent(this.piboSessionId, candidate?.message) : undefined;
|
|
866
|
-
if (usageEvent)
|
|
867
|
-
this.emit(this.withActiveMessage(usageEvent));
|
|
868
866
|
// Pi gets the first chance to recover through its short retry/compaction loop.
|
|
869
867
|
// Keep the final error pending so the routed turn can continue durable recovery.
|
|
870
868
|
if (assistantMessageEnded && normalized?.type === "session_error") {
|
|
@@ -889,6 +887,8 @@ export class RoutedSession {
|
|
|
889
887
|
this.emit(this.withActiveMessage(normalized));
|
|
890
888
|
}
|
|
891
889
|
}
|
|
890
|
+
if (usageEvent)
|
|
891
|
+
this.emit(this.withActiveMessage(usageEvent));
|
|
892
892
|
if (this.forwardPiEvents) {
|
|
893
893
|
this.emit({ type: "pi_event", piboSessionId: this.piboSessionId, event });
|
|
894
894
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/** Owns the only application queue; HWM zero prevents a second hidden stream queue. */
|
|
2
|
+
export class BoundedEventStream {
|
|
3
|
+
onClose;
|
|
4
|
+
maximumBytes;
|
|
5
|
+
maximumAgeMs;
|
|
6
|
+
maximumFrames;
|
|
7
|
+
stream;
|
|
8
|
+
writer;
|
|
9
|
+
controller;
|
|
10
|
+
queue = [];
|
|
11
|
+
bytes = 0;
|
|
12
|
+
stopped = false;
|
|
13
|
+
finishing = false;
|
|
14
|
+
pendingPull;
|
|
15
|
+
capacityWaiter;
|
|
16
|
+
timer;
|
|
17
|
+
constructor(onClose, maximumBytes = 2 * 1024 * 1024, maximumAgeMs = 5000, maximumFrames = 4096) {
|
|
18
|
+
this.onClose = onClose;
|
|
19
|
+
this.maximumBytes = maximumBytes;
|
|
20
|
+
this.maximumAgeMs = maximumAgeMs;
|
|
21
|
+
this.maximumFrames = maximumFrames;
|
|
22
|
+
this.stream = new ReadableStream({
|
|
23
|
+
start: controller => { this.controller = controller; },
|
|
24
|
+
pull: () => {
|
|
25
|
+
const frame = this.queue.shift();
|
|
26
|
+
if (frame) {
|
|
27
|
+
this.bytes -= frame.value.byteLength;
|
|
28
|
+
this.controller.enqueue(frame.value);
|
|
29
|
+
this.releaseCapacity();
|
|
30
|
+
if (this.finishing && !this.queue.length)
|
|
31
|
+
this.stop("complete");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (this.finishing || this.stopped) {
|
|
35
|
+
this.stop("complete");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
return new Promise(resolve => { this.pendingPull = resolve; });
|
|
39
|
+
},
|
|
40
|
+
cancel: () => this.stop("cancel"),
|
|
41
|
+
}, { highWaterMark: 0 });
|
|
42
|
+
const self = this;
|
|
43
|
+
this.writer = { enqueue: value => { this.enqueue(value); }, close: () => this.finish(), error: () => this.stop("error"), get desiredSize() { return self.stopped ? null : self.maximumBytes - self.bytes; } };
|
|
44
|
+
this.timer = setInterval(() => { if (this.queue.length && performance.now() - this.queue[0].at > this.maximumAgeMs)
|
|
45
|
+
this.stop("slow_age"); }, Math.min(1000, Math.max(5, maximumAgeMs / 2)));
|
|
46
|
+
this.timer.unref();
|
|
47
|
+
}
|
|
48
|
+
get closed() { return this.stopped || this.finishing; }
|
|
49
|
+
enqueue(value) {
|
|
50
|
+
if (this.closed)
|
|
51
|
+
return;
|
|
52
|
+
if (value.byteLength > this.maximumBytes || this.bytes + value.byteLength > this.maximumBytes || this.queue.length >= this.maximumFrames) {
|
|
53
|
+
this.stop("slow_bytes");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (this.pendingPull) {
|
|
57
|
+
const resolve = this.pendingPull;
|
|
58
|
+
this.pendingPull = undefined;
|
|
59
|
+
this.controller.enqueue(value);
|
|
60
|
+
resolve();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.queue.push({ value, at: performance.now() });
|
|
64
|
+
this.bytes += value.byteLength;
|
|
65
|
+
}
|
|
66
|
+
async waitForCapacity() { if (this.closed)
|
|
67
|
+
return false; if (this.bytes < this.maximumBytes / 2)
|
|
68
|
+
return true; return new Promise(resolve => { this.capacityWaiter = resolve; }); }
|
|
69
|
+
releaseCapacity() { if (this.bytes < this.maximumBytes / 2) {
|
|
70
|
+
this.capacityWaiter?.(!this.closed);
|
|
71
|
+
this.capacityWaiter = undefined;
|
|
72
|
+
} }
|
|
73
|
+
finish() { if (this.stopped)
|
|
74
|
+
return; this.finishing = true; if (!this.queue.length)
|
|
75
|
+
this.stop("complete"); }
|
|
76
|
+
fail() { this.stop("error"); }
|
|
77
|
+
stop(reason) {
|
|
78
|
+
if (this.stopped)
|
|
79
|
+
return;
|
|
80
|
+
this.stopped = true;
|
|
81
|
+
clearInterval(this.timer);
|
|
82
|
+
this.queue.length = 0;
|
|
83
|
+
this.bytes = 0;
|
|
84
|
+
try {
|
|
85
|
+
if (reason === "complete")
|
|
86
|
+
this.controller.close();
|
|
87
|
+
else if (reason !== "cancel")
|
|
88
|
+
this.controller.error(Error("Event stream budget exhausted; reconnect using the last received cursor."));
|
|
89
|
+
}
|
|
90
|
+
catch { }
|
|
91
|
+
this.pendingPull?.();
|
|
92
|
+
this.pendingPull = undefined;
|
|
93
|
+
this.capacityWaiter?.(false);
|
|
94
|
+
this.capacityWaiter = undefined;
|
|
95
|
+
this.onClose(reason);
|
|
96
|
+
}
|
|
97
|
+
status() { return { frames: this.queue.length, bytes: this.bytes, oldestAgeMs: this.queue.length ? performance.now() - this.queue[0].at : 0, closed: this.closed }; }
|
|
98
|
+
}
|
|
@@ -8,7 +8,7 @@ import { parseToolMetricTokenCalculation } from "../../shared/tool-call-token-se
|
|
|
8
8
|
import { PiboWebHttpError, readJsonBody, responseJson } from "../../web/http.js";
|
|
9
9
|
import { CHAT_WEB_API_PREFIX } from "./chat-api-routes.js";
|
|
10
10
|
import { normalizeBasePromptMarkdown, normalizeBasePromptMode, normalizeCompactionPromptMarkdown, normalizeCompactionPromptMode, updateChatModelDefaults, } from "./chat-request-normalizers.js";
|
|
11
|
-
import {
|
|
11
|
+
import { pruneTelemetryOlderThanAsync } from "./telemetry-retention-service.js";
|
|
12
12
|
export function chatSettingsRoute(pathname, method) {
|
|
13
13
|
if (pathname === `${CHAT_WEB_API_PREFIX}/model-defaults` && method === "PATCH")
|
|
14
14
|
return { kind: "model-defaults" };
|
|
@@ -70,8 +70,8 @@ export async function handleChatSettingsRoute(input) {
|
|
|
70
70
|
const days = sanitizeTelemetryRetentionDays(body.days);
|
|
71
71
|
if (!days)
|
|
72
72
|
throw new PiboWebHttpError("Invalid telemetry retention days", 400);
|
|
73
|
-
const result =
|
|
74
|
-
if (result.applied)
|
|
73
|
+
const result = await pruneTelemetryOlderThanAsync({ dataStore: input.dataStore, days, apply: body.dryRun !== true, signal: request.signal });
|
|
74
|
+
if (result.applied && result.completed !== false)
|
|
75
75
|
updateTelemetryRetentionLastPrunedAt(new Date().toISOString());
|
|
76
76
|
return responseJson({ telemetryRetention: result });
|
|
77
77
|
}
|
|
@@ -3,13 +3,31 @@ import { roomWorkspaceFromMetadata } from "../types/rooms.js";
|
|
|
3
3
|
import { tracePayloadRefForStoredPayload } from "../trace-v2.js";
|
|
4
4
|
import { qualifiedToolNodeId } from "../../../shared/trace-tool-identity.js";
|
|
5
5
|
const MAX_TRACE_EVENT_HYDRATION_BYTES = 1024 * 1024;
|
|
6
|
-
export function storedPiboEventFromV2Row(row, payloadStore) {
|
|
6
|
+
export function storedPiboEventFromV2Row(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
|
|
7
|
+
if (typeof hydrationBytes !== "number" || !Number.isFinite(hydrationBytes))
|
|
8
|
+
hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES;
|
|
7
9
|
const attributes = parseJsonObject(row.attributes_json);
|
|
8
|
-
|
|
10
|
+
if (hydrationBytes < MAX_TRACE_EVENT_HYDRATION_BYTES && typeof attributes.inlineText === "string" && Buffer.byteLength(attributes.inlineText) > hydrationBytes)
|
|
11
|
+
attributes.inlineText = row.preview_text ?? "";
|
|
12
|
+
const payload = outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore, hydrationBytes));
|
|
9
13
|
if (!payload)
|
|
10
14
|
return undefined;
|
|
15
|
+
const storedPayloadRef = storedPayloadReference(row, attributes, payloadStore);
|
|
16
|
+
const renderSequence = numberAttribute(attributes, "renderSequence") ?? payload.renderSequence;
|
|
17
|
+
return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, renderSequence, eventId: row.event_id ?? undefined, streamId: row.stream_id, storedPayloadRef, type: row.type, createdAt: row.created_at, payload };
|
|
18
|
+
}
|
|
19
|
+
export function storedChatEventFromV2Row(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
|
|
20
|
+
if (typeof hydrationBytes !== "number" || !Number.isFinite(hydrationBytes))
|
|
21
|
+
hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES;
|
|
22
|
+
const attributes = parseJsonObject(row.attributes_json);
|
|
23
|
+
if (hydrationBytes < MAX_TRACE_EVENT_HYDRATION_BYTES && typeof attributes.inlineText === "string" && Buffer.byteLength(attributes.inlineText) > hydrationBytes)
|
|
24
|
+
attributes.inlineText = row.preview_text ?? "";
|
|
25
|
+
const storedPayloadRef = storedPayloadReference(row, attributes, payloadStore);
|
|
26
|
+
return { storedPayloadRef, streamId: row.stream_id, roomId: row.room_id ?? undefined, piboSessionId: row.session_id ?? undefined, eventId: row.event_id ?? `evt_${row.stream_id}`, eventType: row.type, actorType: actorTypeValue(row.actor_type), actorId: row.actor_id ?? undefined, clientTxnId: typeof attributes.clientTxnId === "string" ? attributes.clientTxnId : undefined, createdAt: row.created_at, retentionClass: retentionClassValue(row.retention_class), payload: (outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore, hydrationBytes)) ?? null) };
|
|
27
|
+
}
|
|
28
|
+
function storedPayloadReference(row, attributes, payloadStore) {
|
|
11
29
|
const payloadIdentity = tracePayloadIdentityForEvent(row, attributes);
|
|
12
|
-
|
|
30
|
+
return row.payload_ref && row.session_id && payloadStore && payloadIdentity
|
|
13
31
|
? tracePayloadRefForStoredPayload({
|
|
14
32
|
payloadStore,
|
|
15
33
|
piboSessionId: row.session_id,
|
|
@@ -17,12 +35,6 @@ export function storedPiboEventFromV2Row(row, payloadStore) {
|
|
|
17
35
|
...payloadIdentity,
|
|
18
36
|
})
|
|
19
37
|
: undefined;
|
|
20
|
-
const renderSequence = numberAttribute(attributes, "renderSequence") ?? payload.renderSequence;
|
|
21
|
-
return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, renderSequence, eventId: row.event_id ?? undefined, streamId: row.stream_id, storedPayloadRef, type: row.type, createdAt: row.created_at, payload };
|
|
22
|
-
}
|
|
23
|
-
export function storedChatEventFromV2Row(row, payloadStore) {
|
|
24
|
-
const attributes = parseJsonObject(row.attributes_json);
|
|
25
|
-
return { streamId: row.stream_id, roomId: row.room_id ?? undefined, piboSessionId: row.session_id ?? undefined, eventId: row.event_id ?? `evt_${row.stream_id}`, eventType: row.type, actorType: actorTypeValue(row.actor_type), actorId: row.actor_id ?? undefined, clientTxnId: typeof attributes.clientTxnId === "string" ? attributes.clientTxnId : undefined, createdAt: row.created_at, retentionClass: retentionClassValue(row.retention_class), payload: (outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore)) ?? null) };
|
|
26
38
|
}
|
|
27
39
|
function outputPayloadFromV2Row(row, attributes, persistedPayload) {
|
|
28
40
|
const inlinePayload = attributes.inlinePayload ?? persistedPayload;
|
|
@@ -112,7 +124,7 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
|
|
|
112
124
|
if (row.type === "compaction_start")
|
|
113
125
|
return { ...base, type: "compaction_start", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown" };
|
|
114
126
|
if (row.type === "compaction_end")
|
|
115
|
-
return { ...base, type: "compaction_end", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown", result: inlinePayload, aborted: booleanAttribute(attributes, "aborted") ?? false, errorMessage: stringAttribute(attributes, "errorMessage") };
|
|
127
|
+
return { ...base, type: "compaction_end", compactionIndex: numberAttribute(attributes, "compactionIndex"), reason: stringAttribute(attributes, "reason") ?? row.preview_text ?? "unknown", result: inlinePayload, aborted: booleanAttribute(attributes, "aborted") ?? false, errorMessage: stringAttribute(attributes, "errorMessage"), compactionStats: isRecord(attributes.compactionStats) ? attributes.compactionStats : undefined };
|
|
116
128
|
if (row.type === "session_error") {
|
|
117
129
|
const error = stringAttribute(attributes, "error") ?? row.preview_text ?? "Error";
|
|
118
130
|
return { ...base, type: "session_error", error, errorDetails: normalizeSessionErrorDetails(error, isRecord(attributes.errorDetails) ? attributes.errorDetails : undefined) };
|
|
@@ -121,14 +133,14 @@ function outputPayloadFromV2Row(row, attributes, persistedPayload) {
|
|
|
121
133
|
return { type: "user.message.accepted", piboSessionId, roomId: row.room_id ?? undefined, text: stringAttribute(attributes, "inlineText") ?? row.preview_text ?? "", clientTxnId: stringAttribute(attributes, "clientTxnId") };
|
|
122
134
|
return { ...base, type: row.type };
|
|
123
135
|
}
|
|
124
|
-
function readPersistedPayload(row, payloadStore) {
|
|
136
|
+
function readPersistedPayload(row, payloadStore, hydrationBytes = MAX_TRACE_EVENT_HYDRATION_BYTES) {
|
|
125
137
|
if (!row.payload_ref || !payloadStore)
|
|
126
138
|
return undefined;
|
|
127
139
|
try {
|
|
128
140
|
const metadata = payloadStore.getPayload(row.payload_ref);
|
|
129
|
-
if (!metadata || metadata.byteSize >
|
|
141
|
+
if (!metadata || metadata.byteSize > hydrationBytes)
|
|
130
142
|
return undefined;
|
|
131
|
-
const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref,
|
|
143
|
+
const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref, hydrationBytes)).toString("utf8");
|
|
132
144
|
return metadata.contentType.includes("json") ? JSON.parse(text) : text;
|
|
133
145
|
}
|
|
134
146
|
catch {
|
|
@@ -136,6 +148,13 @@ function readPersistedPayload(row, payloadStore) {
|
|
|
136
148
|
}
|
|
137
149
|
}
|
|
138
150
|
function tracePayloadIdentityForEvent(row, attributes) {
|
|
151
|
+
const semanticId = stringAttribute(attributes, "semanticEventId") ?? row.event_id;
|
|
152
|
+
if (semanticId && (row.type === "assistant_message" || row.type === "thinking_finished")) {
|
|
153
|
+
const thinking = row.type === "thinking_finished";
|
|
154
|
+
const index = numberAttribute(attributes, thinking ? "thinkingIndex" : "assistantIndex") ?? numberAttribute(attributes, "contentIndex");
|
|
155
|
+
const identity = index === undefined ? semanticId : `${semanticId}:${thinking ? "thinking" : "assistant"}:${index}`;
|
|
156
|
+
return { nodeId: `${thinking ? "reasoning" : "assistant"}:${identity}`, payloadKind: thinking ? "reasoning" : "output" };
|
|
157
|
+
}
|
|
139
158
|
const toolCallId = stringAttribute(attributes, "toolCallId");
|
|
140
159
|
if (!toolCallId)
|
|
141
160
|
return undefined;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { isLiveOnlyOutputEvent } from "../output-event-policy.js";
|
|
3
3
|
import { compactObject, nextSessionSequence, previewForPayload, storedChatEventFromV2Row } from "./chat-data-mappers.js";
|
|
4
|
+
export function chatClientTransactionKey(roomId, actorId, clientTxnId) {
|
|
5
|
+
return `chat:user.accepted:${roomId ?? ""}:${actorId ?? ""}:${clientTxnId}`;
|
|
6
|
+
}
|
|
4
7
|
export class ChatEventCommandService {
|
|
5
8
|
store;
|
|
6
9
|
constructor(store) {
|
|
@@ -9,10 +12,7 @@ export class ChatEventCommandService {
|
|
|
9
12
|
appendEvent(input) {
|
|
10
13
|
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
11
14
|
const eventId = input.eventId ?? `ce_${randomUUID()}`;
|
|
12
|
-
const idempotencyKey = input.clientTxnId ?
|
|
13
|
-
const existing = input.clientTxnId ? this.findByClientTxn(input.roomId, input.actorId, input.clientTxnId) : undefined;
|
|
14
|
-
if (existing)
|
|
15
|
-
return existing;
|
|
15
|
+
const idempotencyKey = input.clientTxnId ? chatClientTransactionKey(input.roomId, input.actorId, input.clientTxnId) : `chat:event:${eventId}`;
|
|
16
16
|
const stored = this.store.eventLog.appendEvent({
|
|
17
17
|
sessionId: input.piboSessionId,
|
|
18
18
|
sessionSequence: input.piboSessionId ? nextSessionSequence(this.store, input.piboSessionId) : undefined,
|
|
@@ -30,21 +30,7 @@ export class ChatEventCommandService {
|
|
|
30
30
|
createdAt,
|
|
31
31
|
indexedAt: createdAt,
|
|
32
32
|
});
|
|
33
|
-
return
|
|
34
|
-
stream_id: stored.streamId,
|
|
35
|
-
session_id: stored.sessionId ?? null,
|
|
36
|
-
session_sequence: stored.sessionSequence ?? null,
|
|
37
|
-
room_id: stored.roomId ?? null,
|
|
38
|
-
type: stored.type,
|
|
39
|
-
actor_type: stored.actorType ?? null,
|
|
40
|
-
actor_id: stored.actorId ?? null,
|
|
41
|
-
event_id: stored.eventId ?? null,
|
|
42
|
-
idempotency_key: stored.idempotencyKey ?? null,
|
|
43
|
-
retention_class: stored.retentionClass,
|
|
44
|
-
preview_text: stored.previewText ?? null,
|
|
45
|
-
attributes_json: JSON.stringify(stored.attributes),
|
|
46
|
-
created_at: stored.createdAt,
|
|
47
|
-
});
|
|
33
|
+
return chatEventFromStoredRow(stored);
|
|
48
34
|
}
|
|
49
35
|
appendOutputEvent(event, _input = {}) {
|
|
50
36
|
// Runtime output ingestion is owned by ChatDataIngestService. Avoid a second event_log insert here.
|
|
@@ -55,8 +41,14 @@ export class ChatEventCommandService {
|
|
|
55
41
|
findByClientTxn(roomId, actorId, clientTxnId) {
|
|
56
42
|
if (!roomId || !actorId)
|
|
57
43
|
return undefined;
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
// Empty IDs are not valid HTTP transaction keys. Retain the legacy direct-call
|
|
45
|
+
// lookup contract without adding a scan fallback to ordinary indexed misses.
|
|
46
|
+
if (clientTxnId === "") {
|
|
47
|
+
const row = this.store.db.prepare("SELECT * FROM event_log WHERE room_id = ? AND actor_id = ? AND json_extract(attributes_json, '$.clientTxnId') = ? ORDER BY stream_id ASC LIMIT 1").get(roomId, actorId, clientTxnId);
|
|
48
|
+
return row ? storedChatEventFromV2Row(row) : undefined;
|
|
49
|
+
}
|
|
50
|
+
const row = this.store.eventLog.findByIdempotencyKey(chatClientTransactionKey(roomId, actorId, clientTxnId));
|
|
51
|
+
return row ? chatEventFromStoredRow(row) : undefined;
|
|
60
52
|
}
|
|
61
53
|
deleteSessions(piboSessionIds) {
|
|
62
54
|
if (!piboSessionIds.length)
|
|
@@ -73,3 +65,20 @@ export class ChatEventCommandService {
|
|
|
73
65
|
return Number(result.changes ?? 0);
|
|
74
66
|
}
|
|
75
67
|
}
|
|
68
|
+
function chatEventFromStoredRow(stored) {
|
|
69
|
+
return storedChatEventFromV2Row({
|
|
70
|
+
stream_id: stored.streamId,
|
|
71
|
+
session_id: stored.sessionId ?? null,
|
|
72
|
+
session_sequence: stored.sessionSequence ?? null,
|
|
73
|
+
room_id: stored.roomId ?? null,
|
|
74
|
+
type: stored.type,
|
|
75
|
+
actor_type: stored.actorType ?? null,
|
|
76
|
+
actor_id: stored.actorId ?? null,
|
|
77
|
+
event_id: stored.eventId ?? null,
|
|
78
|
+
idempotency_key: stored.idempotencyKey ?? null,
|
|
79
|
+
retention_class: stored.retentionClass,
|
|
80
|
+
preview_text: stored.previewText ?? null,
|
|
81
|
+
attributes_json: JSON.stringify(stored.attributes),
|
|
82
|
+
created_at: stored.createdAt,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { tracePayloadRefForStoredPayload } from "../trace-v2.js";
|
|
2
|
+
import { ChatReadProjectionStore } from "../../../data/chat-read-projections.js";
|
|
1
3
|
export class ChatHistoryQueryService {
|
|
2
4
|
store;
|
|
3
|
-
|
|
5
|
+
pageByteBudget;
|
|
6
|
+
constructor(store, pageByteBudget = Number.POSITIVE_INFINITY) {
|
|
4
7
|
this.store = store;
|
|
8
|
+
this.pageByteBudget = pageByteBudget;
|
|
5
9
|
}
|
|
6
10
|
listProductHistoryEntries(input) {
|
|
7
11
|
const limit = Math.max(1, Math.min(input.limit ?? 200, 1000));
|
|
@@ -11,7 +15,12 @@ export class ChatHistoryQueryService {
|
|
|
11
15
|
clauses.push("COALESCE(e.session_sequence, m.sequence) < ?");
|
|
12
16
|
values.push(input.beforeSequence);
|
|
13
17
|
}
|
|
14
|
-
const
|
|
18
|
+
const complete = new ChatReadProjectionStore(this.store.db).status().historyComplete;
|
|
19
|
+
const rows = complete ? this.store.db.prepare(`
|
|
20
|
+
SELECT m.*,h.event_sequence FROM chat_history_index h JOIN chat_messages m ON m.id=h.message_id
|
|
21
|
+
WHERE h.session_id=? AND h.role IN ('user','assistant','system') ${input.beforeSequence !== undefined ? "AND h.event_sequence < ?" : ""}
|
|
22
|
+
ORDER BY h.event_sequence DESC,h.message_id DESC LIMIT ?
|
|
23
|
+
`).all(input.piboSessionId, ...(input.beforeSequence !== undefined ? [input.beforeSequence] : []), limit) : this.store.db.prepare(`
|
|
15
24
|
SELECT m.*, e.session_sequence AS event_sequence
|
|
16
25
|
FROM chat_messages m
|
|
17
26
|
LEFT JOIN event_log e ON e.stream_id = m.source_stream_id
|
|
@@ -24,7 +33,9 @@ export class ChatHistoryQueryService {
|
|
|
24
33
|
if (!role)
|
|
25
34
|
return [];
|
|
26
35
|
const attributes = parseObject(row.attributes_json);
|
|
27
|
-
const text = this.readMessageText(row, attributes);
|
|
36
|
+
const text = this.readMessageText(row, attributes, Math.min(64 * 1024, Math.floor(this.pageByteBudget / limit)));
|
|
37
|
+
const contentBytes = row.content_payload_ref ? this.store.payloads.getPayload(row.content_payload_ref)?.byteSize : typeof attributes.inlineText === "string" ? Buffer.byteLength(attributes.inlineText) : Buffer.byteLength(row.content_preview ?? "");
|
|
38
|
+
const contentTruncated = contentBytes !== undefined && Buffer.byteLength(text) < contentBytes;
|
|
28
39
|
return [{
|
|
29
40
|
id: `product:${row.id}`,
|
|
30
41
|
type: "message",
|
|
@@ -42,42 +53,40 @@ export class ChatHistoryQueryService {
|
|
|
42
53
|
sourceStreamId: row.source_stream_id ?? undefined,
|
|
43
54
|
completedAt: row.completed_at ?? undefined,
|
|
44
55
|
payloadRef: row.content_payload_ref ?? undefined,
|
|
56
|
+
tracePayloadRef: contentTruncated && row.content_payload_ref ? tracePayloadRefForStoredPayload({ payloadStore: this.store.payloads, piboSessionId: row.session_id, payloadId: row.content_payload_ref, nodeId: `product:${row.id}`, payloadKind: "output" }) : undefined,
|
|
57
|
+
contentTruncated: contentTruncated || undefined,
|
|
58
|
+
contentBytes: contentTruncated ? contentBytes : undefined,
|
|
45
59
|
}),
|
|
46
60
|
}];
|
|
47
61
|
});
|
|
48
62
|
}
|
|
49
63
|
getProductHistoryCoverage(piboSessionId) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
LEFT JOIN event_log e ON e.stream_id = m.source_stream_id
|
|
59
|
-
WHERE m.session_id = ?
|
|
60
|
-
`).get(piboSessionId);
|
|
61
|
-
return {
|
|
62
|
-
messageCount: Number(row.message_count ?? 0),
|
|
63
|
-
firstEventSequence: row.first_event_sequence ?? undefined,
|
|
64
|
-
lastEventSequence: row.last_event_sequence ?? undefined,
|
|
65
|
-
firstCreatedAt: row.first_created_at ?? undefined,
|
|
66
|
-
lastCreatedAt: row.last_created_at ?? undefined,
|
|
67
|
-
};
|
|
64
|
+
const complete = new ChatReadProjectionStore(this.store.db).status().historyComplete;
|
|
65
|
+
const count = this.store.db.prepare("SELECT message_count,revision FROM chat_history_counts WHERE session_id=?").get(piboSessionId);
|
|
66
|
+
const edge = (column, order) => this.store.db.prepare(`SELECT ${column} AS value FROM chat_history_index WHERE session_id=? ORDER BY ${column} ${order},message_id ${order} LIMIT 1`).get(piboSessionId);
|
|
67
|
+
return { messageCount: count?.message_count ?? 0, revision: count?.revision ?? 0, complete,
|
|
68
|
+
firstEventSequence: edge("event_sequence", "ASC")?.value,
|
|
69
|
+
lastEventSequence: edge("event_sequence", "DESC")?.value,
|
|
70
|
+
firstCreatedAt: edge("created_at", "ASC")?.value,
|
|
71
|
+
lastCreatedAt: edge("created_at", "DESC")?.value };
|
|
68
72
|
}
|
|
69
|
-
readMessageText(row, attributes) {
|
|
73
|
+
readMessageText(row, attributes, maximum) {
|
|
70
74
|
if (row.content_payload_ref) {
|
|
71
75
|
try {
|
|
72
|
-
|
|
76
|
+
if (!Number.isFinite(this.pageByteBudget))
|
|
77
|
+
return this.store.payloads.readPayloadText(row.content_payload_ref);
|
|
78
|
+
const payload = this.store.payloads.getPayload(row.content_payload_ref);
|
|
79
|
+
if (!payload || payload.byteSize > maximum)
|
|
80
|
+
return (row.content_preview ?? "").slice(0, Math.min(2048, maximum / 4));
|
|
81
|
+
return Buffer.from(this.store.payloads.readPayloadBytesBounded(row.content_payload_ref, maximum)).toString("utf8");
|
|
73
82
|
}
|
|
74
83
|
catch {
|
|
75
84
|
// Retain the durable preview if the external payload was removed or corrupted.
|
|
76
85
|
}
|
|
77
86
|
}
|
|
78
87
|
if (typeof attributes.inlineText === "string")
|
|
79
|
-
return attributes.inlineText;
|
|
80
|
-
return row.content_preview ?? "";
|
|
88
|
+
return Number.isFinite(this.pageByteBudget) ? attributes.inlineText.slice(0, Math.floor(maximum / 4)) : attributes.inlineText;
|
|
89
|
+
return Number.isFinite(this.pageByteBudget) ? (row.content_preview ?? "").slice(0, Math.floor(maximum / 4)) : (row.content_preview ?? "");
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
92
|
function historyRole(role) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ChatSessionQueryService } from "./session-query-service.js";
|
|
2
|
+
import { ChatReadProjectionStore } from "../../../data/chat-read-projections.js";
|
|
1
3
|
export class ChatReadStateService {
|
|
2
4
|
store;
|
|
3
5
|
constructor(store) {
|
|
@@ -14,11 +16,22 @@ export class ChatReadStateService {
|
|
|
14
16
|
updated_at = excluded.updated_at
|
|
15
17
|
`).run(piboSessionId, lastReadStreamId, now, now);
|
|
16
18
|
}
|
|
19
|
+
sessionIndexPage(input) { return new ChatSessionQueryService(this.store).listSessionIndexPage(input); }
|
|
20
|
+
unreadCountsPage(input) { return [...this.countUnreadMessagesBySession(input)]; }
|
|
17
21
|
countUnreadMessagesBySession(input) {
|
|
18
22
|
const counts = new Map();
|
|
19
23
|
const uniqueIds = [...new Set(input.piboSessionIds)];
|
|
20
24
|
if (!uniqueIds.length)
|
|
21
25
|
return counts;
|
|
26
|
+
if (new ChatReadProjectionStore(this.store.db).status().unreadComplete) {
|
|
27
|
+
for (let offset = 0; offset < uniqueIds.length; offset += 400) {
|
|
28
|
+
const ids = uniqueIds.slice(offset, offset + 400);
|
|
29
|
+
const rows = this.store.db.prepare(`SELECT session_id,unread_count FROM chat_unread_counts WHERE session_id IN (${ids.map(() => "?").join(",")}) AND unread_count>0`).all(...ids);
|
|
30
|
+
for (const row of rows)
|
|
31
|
+
counts.set(row.session_id, row.unread_count);
|
|
32
|
+
}
|
|
33
|
+
return counts;
|
|
34
|
+
}
|
|
22
35
|
for (let offset = 0; offset < uniqueIds.length; offset += 400) {
|
|
23
36
|
const ids = uniqueIds.slice(offset, offset + 400);
|
|
24
37
|
const requested = ids.map(() => "(?)").join(", ");
|
|
@@ -6,20 +6,21 @@ export class ChatSessionQueryService {
|
|
|
6
6
|
constructor(store) {
|
|
7
7
|
this.store = store;
|
|
8
8
|
}
|
|
9
|
-
upsertSession(session, status = "idle", lastActivityAt = session.updatedAt) {
|
|
9
|
+
upsertSession(session, status = "idle", lastActivityAt = session.updatedAt, options = {}) {
|
|
10
10
|
const roomId = chatRoomIdFromMetadata(session.metadata) ?? "room_default";
|
|
11
|
-
this.store.sessions.upsertSession({ session, roomId, status, lastActivityAt });
|
|
11
|
+
this.store.sessions.upsertSession({ session, roomId, status, lastActivityAt, preserveRuntimeBinding: options.preserveRuntimeBinding });
|
|
12
12
|
this.upsertNavigation(session, roomId, status, lastActivityAt);
|
|
13
13
|
}
|
|
14
14
|
upsertSessionsIfChanged(sessions) {
|
|
15
15
|
let written = 0;
|
|
16
16
|
let skipped = 0;
|
|
17
17
|
for (const session of sessions) {
|
|
18
|
-
|
|
18
|
+
const status = this.getSession(session.id)?.status ?? "idle";
|
|
19
|
+
if (this.sessionIndexMatches(session, status)) {
|
|
19
20
|
skipped++;
|
|
20
21
|
continue;
|
|
21
22
|
}
|
|
22
|
-
this.upsertSession(session);
|
|
23
|
+
this.upsertSession(session, status);
|
|
23
24
|
written++;
|
|
24
25
|
}
|
|
25
26
|
return { checked: sessions.length, written, skipped };
|
|
@@ -28,7 +29,7 @@ export class ChatSessionQueryService {
|
|
|
28
29
|
if (session) {
|
|
29
30
|
const status = statusFromOutputEvent(event);
|
|
30
31
|
if (status)
|
|
31
|
-
this.upsertSession(session, status, createdAt);
|
|
32
|
+
this.upsertSession(session, status, createdAt, { preserveRuntimeBinding: true });
|
|
32
33
|
else
|
|
33
34
|
this.touchSession(session, createdAt);
|
|
34
35
|
}
|
|
@@ -36,7 +37,7 @@ export class ChatSessionQueryService {
|
|
|
36
37
|
}
|
|
37
38
|
touchSession(session, lastActivityAt) {
|
|
38
39
|
const roomId = chatRoomIdFromMetadata(session.metadata) ?? "room_default";
|
|
39
|
-
this.store.sessions.upsertSession({ session, roomId, lastActivityAt });
|
|
40
|
+
this.store.sessions.upsertSession({ session, roomId, lastActivityAt, preserveRuntimeBinding: true });
|
|
40
41
|
this.store.navigation.upsertSession({
|
|
41
42
|
roomId,
|
|
42
43
|
sessionId: session.id,
|
|
@@ -50,19 +51,26 @@ export class ChatSessionQueryService {
|
|
|
50
51
|
updatedAt: lastActivityAt,
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
|
-
listSessions() {
|
|
54
|
+
listSessions(roomId) {
|
|
54
55
|
const rows = this.store.db.prepare(`
|
|
55
|
-
SELECT s
|
|
56
|
+
SELECT s.id,s.pi_session_id,s.parent_id,s.channel,s.kind,s.profile,s.created_at,s.updated_at,s.last_activity_at,s.status,b.runtime_instance_id,b.runtime_adapter_id,b.native_session_id,b.binding_state
|
|
56
57
|
FROM sessions s
|
|
57
58
|
LEFT JOIN session_runtime_bindings b ON b.pibo_session_id = s.id
|
|
58
|
-
WHERE s.deleted_at IS NULL
|
|
59
|
+
WHERE s.deleted_at IS NULL ${roomId ? "AND s.room_id=?" : ""}
|
|
59
60
|
ORDER BY s.last_activity_at DESC, s.created_at DESC
|
|
60
|
-
`).all();
|
|
61
|
+
`).all(...(roomId ? [roomId] : []));
|
|
62
|
+
return rows.map(sessionFromRow);
|
|
63
|
+
}
|
|
64
|
+
listSessionIndexPage(input) {
|
|
65
|
+
const limit = Math.max(1, Math.min(input.limit ?? 500, 500));
|
|
66
|
+
const rows = this.store.db.prepare(`SELECT s.id,s.pi_session_id,s.parent_id,s.channel,s.kind,s.profile,s.created_at,s.updated_at,s.last_activity_at,s.status,b.runtime_instance_id,b.runtime_adapter_id,b.native_session_id,b.binding_state
|
|
67
|
+
FROM sessions s LEFT JOIN session_runtime_bindings b ON b.pibo_session_id=s.id
|
|
68
|
+
WHERE s.deleted_at IS NULL AND s.room_id=? AND s.id>? ORDER BY s.id LIMIT ?`).all(input.roomId, input.afterId ?? "", limit);
|
|
61
69
|
return rows.map(sessionFromRow);
|
|
62
70
|
}
|
|
63
71
|
getSession(piboSessionId) {
|
|
64
72
|
const row = this.store.db.prepare(`
|
|
65
|
-
SELECT s
|
|
73
|
+
SELECT s.id,s.pi_session_id,s.parent_id,s.channel,s.kind,s.profile,s.created_at,s.updated_at,s.last_activity_at,s.status,b.runtime_instance_id,b.runtime_adapter_id,b.native_session_id,b.binding_state
|
|
66
74
|
FROM sessions s
|
|
67
75
|
LEFT JOIN session_runtime_bindings b ON b.pibo_session_id = s.id
|
|
68
76
|
WHERE s.id = ? AND s.deleted_at IS NULL
|