@pasko70/pibo 2.5.2 → 3.0.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.
Files changed (100) hide show
  1. package/context/pibo-native-tooling.md +1 -0
  2. package/dist/agent-runtime/contract.js +2 -1
  3. package/dist/agent-runtime/history.js +53 -1
  4. package/dist/agent-runtime/routed-session.js +100 -26
  5. package/dist/agent-runtimes/codex-native/adapter.js +304 -30
  6. package/dist/agent-runtimes/codex-native/client.js +22 -3
  7. package/dist/agent-runtimes/codex-native/first-use.js +294 -0
  8. package/dist/agent-runtimes/codex-native/history.js +26 -7
  9. package/dist/agent-runtimes/codex-native/process.js +14 -0
  10. package/dist/agent-runtimes/codex-native/thread.js +59 -25
  11. package/dist/agent-runtimes/codex-native/turn.js +27 -0
  12. package/dist/agent-runtimes/history-proof.js +8 -0
  13. package/dist/agent-runtimes/omp/adapter.js +61 -14
  14. package/dist/agent-runtimes/omp/history.js +189 -34
  15. package/dist/agent-runtimes/omp/thread.js +82 -36
  16. package/dist/agent-runtimes/pi/adapter.js +30 -3
  17. package/dist/agent-runtimes/pi/history.js +66 -5
  18. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  19. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  20. package/dist/apps/chat/chat-api-routes.js +1 -1
  21. package/dist/apps/chat/chat-files.js +130 -9
  22. package/dist/apps/chat/chat-settings-routes.js +27 -2
  23. package/dist/apps/chat/chat-speech.js +100 -0
  24. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  25. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  26. package/dist/apps/chat/trace-v2.js +210 -8
  27. package/dist/apps/chat/trace.js +3 -0
  28. package/dist/apps/chat/web-app.js +108 -3
  29. package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  38. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  39. package/dist/apps/chat-vscode-web/index.html +2 -2
  40. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  41. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
  42. package/dist/cli.js +49 -5
  43. package/dist/config/config.js +59 -8
  44. package/dist/core/preview-server-settings.js +20 -0
  45. package/dist/core/session-router.js +457 -37
  46. package/dist/core/user-settings.js +10 -0
  47. package/dist/data/payload-store.js +57 -1
  48. package/dist/data/schema.js +18 -1
  49. package/dist/debug/trace.js +5 -0
  50. package/dist/gateway/client.js +368 -65
  51. package/dist/gateway/protocol.js +2 -0
  52. package/dist/gateway/server.js +8 -0
  53. package/dist/gateway/web.js +2 -0
  54. package/dist/index.js +1 -1
  55. package/dist/loops/store.js +6 -4
  56. package/dist/mcp/config.js +89 -17
  57. package/dist/mcp/daemon-client.js +473 -153
  58. package/dist/mcp/daemon.js +401 -37
  59. package/dist/plugins/builtin.js +2 -2
  60. package/dist/plugins/codex-native.js +2 -0
  61. package/dist/plugins/registry.js +259 -1
  62. package/dist/previews/cli.js +345 -0
  63. package/dist/previews/config.js +61 -0
  64. package/dist/previews/manager.js +644 -0
  65. package/dist/previews/network.js +198 -0
  66. package/dist/previews/plugin.js +11 -0
  67. package/dist/previews/process-supervisor.js +38 -0
  68. package/dist/previews/proxy.js +387 -0
  69. package/dist/previews/store.js +750 -0
  70. package/dist/previews/types.js +1 -0
  71. package/dist/previews/web-app.js +446 -0
  72. package/dist/runs/registry.js +22 -0
  73. package/dist/runs/tools.js +46 -7
  74. package/dist/session-ui/terminalRows.js +187 -13
  75. package/dist/sessions/pibo-data-store.js +37 -0
  76. package/dist/sessions/runtime-binding-persistence.js +34 -0
  77. package/dist/sessions/runtime-binding.js +6 -0
  78. package/dist/sessions/sqlite-store.js +37 -0
  79. package/dist/shared/trace-engine.js +12 -64
  80. package/dist/shared/trace-event-projection.js +16 -0
  81. package/dist/shared/trace-history.js +684 -184
  82. package/dist/shared/trace-limits.js +7 -0
  83. package/dist/shared/trace-live-patch.js +61 -0
  84. package/dist/shared/trace-tool-identity.js +43 -0
  85. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  86. package/dist/speech/openai-codex.js +407 -0
  87. package/dist/speech/types.js +8 -0
  88. package/dist/subagents/context.js +5 -3
  89. package/dist/subagents/observations.js +94 -6
  90. package/dist/subagents/tool.js +103 -20
  91. package/dist/tools/codex-image-artifacts.js +24 -0
  92. package/dist/tools/codex-image-generation.js +4 -22
  93. package/dist/web/channel.js +88 -4
  94. package/dist/web/http.js +2 -1
  95. package/npm-shrinkwrap.json +2 -2
  96. package/package.json +1 -1
  97. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  98. package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
  99. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  100. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
@@ -1,9 +1,10 @@
1
- import { createReadStream, existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
1
+ import { closeSync, constants, createReadStream, existsSync, fstatSync, mkdirSync, openSync, readSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
2
2
  import { basename, extname, isAbsolute, relative, resolve } from "node:path";
3
3
  import { Readable } from "node:stream";
4
4
  import { piboHomePath } from "../../core/pibo-home.js";
5
5
  import { protectPrivateDirectorySync, protectPrivateFileSync } from "../../core/private-path.js";
6
6
  import { PiboWebHttpError } from "../../web/http.js";
7
+ import { imageMimeTypeFromBytes, TRACE_IMAGE_MAX_DECODED_BYTES } from "./trace-v2.js";
7
8
  export const CHAT_UPLOAD_DIR = piboHomePath("uploads");
8
9
  const CHAT_FILE_ATTACHMENT_LIMIT = 10;
9
10
  export function prepareChatFileAttachments(input) {
@@ -46,16 +47,31 @@ export async function saveUploadedChatFiles(request) {
46
47
  export function resolveDownloadPath(path, basePath) {
47
48
  return isAbsolute(path) ? resolve(path) : resolve(basePath, path);
48
49
  }
49
- export function responseChatFileDownload(absolutePath) {
50
- let stats;
50
+ export function resolveImagePreviewPath(path, basePath) {
51
+ return resolveImagePreviewPathWithinRoots(resolveDownloadPath(path, basePath), [basePath, CHAT_UPLOAD_DIR]);
52
+ }
53
+ export function resolveImagePreviewPathWithinRoots(path, allowedRootPaths) {
54
+ let canonicalPath;
51
55
  try {
52
- stats = statSync(absolutePath);
56
+ canonicalPath = realpathSync(path);
53
57
  }
54
58
  catch {
55
- throw new PiboWebHttpError("File not found: " + absolutePath, 404);
59
+ throw new PiboWebHttpError("Image preview file was not found", 404);
56
60
  }
57
- if (!stats.isFile())
58
- throw new PiboWebHttpError("Path is not a file: " + absolutePath, 400);
61
+ const allowedRoots = [];
62
+ for (const allowedRoot of allowedRootPaths) {
63
+ if (!existsSync(allowedRoot))
64
+ continue;
65
+ const canonicalRoot = realpathSync(allowedRoot);
66
+ allowedRoots.push(canonicalRoot);
67
+ }
68
+ if (allowedRoots.some((allowedRoot) => isPathInsideRoot(canonicalPath, allowedRoot))) {
69
+ return { path: canonicalPath, allowedRoots };
70
+ }
71
+ throw new PiboWebHttpError("Image preview path is outside its authorized roots", 403);
72
+ }
73
+ export function responseChatFileDownload(absolutePath) {
74
+ const stats = requireChatFile(absolutePath);
59
75
  return new Response(Readable.toWeb(createReadStream(absolutePath)), {
60
76
  headers: {
61
77
  "content-type": contentTypeForDownload(absolutePath),
@@ -65,6 +81,108 @@ export function responseChatFileDownload(absolutePath) {
65
81
  },
66
82
  });
67
83
  }
84
+ export function responseChatImagePreview(absolutePath, allowedRoots) {
85
+ const { bytes, stats } = readBoundedImageFile(absolutePath, TRACE_IMAGE_MAX_DECODED_BYTES, allowedRoots);
86
+ const mimeType = imageMimeTypeFromBytes(bytes.subarray(0, Math.min(32, bytes.byteLength)));
87
+ if (!mimeType)
88
+ throw new PiboWebHttpError("Unsupported image preview format", 415);
89
+ return new Response(bytes, {
90
+ headers: imagePreviewHeaders({
91
+ mimeType,
92
+ contentLength: stats.size,
93
+ filename: basename(absolutePath),
94
+ cacheControl: "no-store",
95
+ }),
96
+ });
97
+ }
98
+ export function responseChatTraceImage(image) {
99
+ return new Response(image.bytes, {
100
+ headers: imagePreviewHeaders({
101
+ mimeType: image.mimeType,
102
+ contentLength: image.bytes.byteLength,
103
+ cacheControl: "private, max-age=31536000, immutable",
104
+ }),
105
+ });
106
+ }
107
+ function requireChatFile(absolutePath) {
108
+ let stats;
109
+ try {
110
+ stats = statSync(absolutePath);
111
+ }
112
+ catch {
113
+ throw new PiboWebHttpError("File not found: " + absolutePath, 404);
114
+ }
115
+ if (!stats.isFile())
116
+ throw new PiboWebHttpError("Path is not a file: " + absolutePath, 400);
117
+ return stats;
118
+ }
119
+ function readBoundedImageFile(absolutePath, maxBytes, allowedRoots) {
120
+ let descriptor;
121
+ try {
122
+ descriptor = openSync(absolutePath, constants.O_RDONLY | constants.O_NOFOLLOW);
123
+ }
124
+ catch (error) {
125
+ if (isFileSystemError(error) && error.code === "ELOOP")
126
+ throw new PiboWebHttpError("Image preview path changed during authorization", 409);
127
+ throw new PiboWebHttpError("File not found: " + absolutePath, 404);
128
+ }
129
+ try {
130
+ const stats = fstatSync(descriptor);
131
+ if (!stats.isFile())
132
+ throw new PiboWebHttpError("Path is not a file: " + absolutePath, 400);
133
+ if (allowedRoots?.length)
134
+ assertOpenedImagePathAuthority(absolutePath, stats, allowedRoots);
135
+ if (stats.size > maxBytes)
136
+ throw new PiboWebHttpError(`Image preview exceeds the ${maxBytes}-byte limit`, 413);
137
+ const bytes = Buffer.alloc(stats.size);
138
+ let offset = 0;
139
+ while (offset < bytes.byteLength) {
140
+ const read = readSync(descriptor, bytes, offset, bytes.byteLength - offset, offset);
141
+ if (read === 0)
142
+ break;
143
+ offset += read;
144
+ }
145
+ if (offset !== bytes.byteLength)
146
+ throw new PiboWebHttpError("Image changed while it was being read", 409);
147
+ const extra = Buffer.alloc(1);
148
+ if (readSync(descriptor, extra, 0, 1, offset) !== 0)
149
+ throw new PiboWebHttpError(`Image preview exceeds the ${maxBytes}-byte limit`, 413);
150
+ return { bytes, stats };
151
+ }
152
+ finally {
153
+ closeSync(descriptor);
154
+ }
155
+ }
156
+ function assertOpenedImagePathAuthority(absolutePath, openedStats, allowedRoots) {
157
+ let currentPath;
158
+ let currentStats;
159
+ try {
160
+ currentPath = realpathSync(absolutePath);
161
+ currentStats = statSync(currentPath);
162
+ }
163
+ catch {
164
+ throw new PiboWebHttpError("Image preview path changed during authorization", 409);
165
+ }
166
+ if (currentStats.dev !== openedStats.dev || currentStats.ino !== openedStats.ino) {
167
+ throw new PiboWebHttpError("Image preview file changed during authorization", 409);
168
+ }
169
+ if (!allowedRoots.some((allowedRoot) => isPathInsideRoot(currentPath, allowedRoot))) {
170
+ throw new PiboWebHttpError("Image preview path escaped its authorized root", 403);
171
+ }
172
+ }
173
+ function isFileSystemError(error) {
174
+ return error instanceof Error && "code" in error;
175
+ }
176
+ function imagePreviewHeaders(input) {
177
+ return {
178
+ "content-type": input.mimeType,
179
+ "content-length": String(input.contentLength),
180
+ ...(input.filename ? { "content-disposition": "inline; filename*=UTF-8''" + encodeURIComponent(input.filename) } : {}),
181
+ "cache-control": input.cacheControl,
182
+ "x-content-type-options": "nosniff",
183
+ "cross-origin-resource-policy": "same-origin",
184
+ };
185
+ }
68
186
  function normalizeChatFileAttachmentPaths(value) {
69
187
  if (value === undefined || value === null)
70
188
  return [];
@@ -100,8 +218,11 @@ function chatFileAttachmentForPath(path) {
100
218
  return { name: basename(path), path, bytes: stats.size };
101
219
  }
102
220
  function isPathInsideUploadDir(path) {
103
- const uploadRelative = relative(CHAT_UPLOAD_DIR, path);
104
- return uploadRelative !== "" && !uploadRelative.startsWith("..") && !isAbsolute(uploadRelative);
221
+ return isPathInsideRoot(path, CHAT_UPLOAD_DIR);
222
+ }
223
+ function isPathInsideRoot(path, root) {
224
+ const child = relative(root, path);
225
+ return child !== "" && child !== ".." && !child.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(child);
105
226
  }
106
227
  function renderAttachedChatFiles(attachments) {
107
228
  const bounded = attachments.slice(0, CHAT_FILE_ATTACHMENT_LIMIT);
@@ -1,6 +1,7 @@
1
1
  import { readPiboBasePrompt, savePiboCustomBasePrompt, setPiboBasePromptMode } from "../../core/base-prompt.js";
2
2
  import { readPiboCompactionPrompt, savePiboCustomCompactionPrompt, setPiboCompactionPromptMode } from "../../core/compaction-prompt.js";
3
3
  import { loadPiboGatewaySettings, sanitizeConcurrentYieldedRuns, updatePiboGatewaySettings, } from "../../core/gateway-settings.js";
4
+ import { sanitizePreviewServerSettings } from "../../core/preview-server-settings.js";
4
5
  import { sanitizeTelemetryRetentionDays, sanitizeTelemetryRetentionSettings } from "../../core/telemetry-retention-settings.js";
5
6
  import { loadPiboUserSettings, sanitizeShortcutSettings, sanitizeTimezone, sanitizeTranscriptionProviderId, updatePiboUserSettings, updateTelemetryRetentionLastPrunedAt } from "../../core/user-settings.js";
6
7
  import { PiboWebHttpError, readJsonBody, responseJson } from "../../web/http.js";
@@ -51,7 +52,9 @@ export async function handleChatSettingsRoute(input) {
51
52
  if (route.action === "read")
52
53
  return responseJson({ userSettings: loadPiboUserSettings() });
53
54
  const body = await readJsonBody(request);
54
- return responseJson({ userSettings: updatePiboUserSettings(userSettingsPatch(body, input.transcriptionProviderIds)) });
55
+ return responseJson({
56
+ userSettings: updatePiboUserSettings(userSettingsPatch(body, input.transcriptionProviderIds, input.speechProviderIds)),
57
+ });
55
58
  }
56
59
  if (route.kind === "gateway-settings") {
57
60
  if (route.action === "read")
@@ -106,7 +109,7 @@ function gatewaySettingsPatch(body) {
106
109
  throw new PiboWebHttpError("No gateway settings provided", 400);
107
110
  return patch;
108
111
  }
109
- function userSettingsPatch(body, transcriptionProviderIds) {
112
+ function userSettingsPatch(body, transcriptionProviderIds, speechProviderIds) {
110
113
  const patch = {};
111
114
  if (body.timezone !== undefined) {
112
115
  const timezone = sanitizeTimezone(body.timezone);
@@ -128,6 +131,28 @@ function userSettingsPatch(body, transcriptionProviderIds) {
128
131
  }
129
132
  patch.transcription = { providerId };
130
133
  }
134
+ if (body.speech !== undefined) {
135
+ const raw = body.speech && typeof body.speech === "object" && !Array.isArray(body.speech)
136
+ ? body.speech
137
+ : {};
138
+ const providerId = sanitizeTranscriptionProviderId(raw.providerId);
139
+ if (!providerId)
140
+ throw new PiboWebHttpError("Invalid speech provider", 400);
141
+ if (speechProviderIds && !speechProviderIds.includes(providerId)) {
142
+ throw new PiboWebHttpError(`Unknown speech provider "${providerId}"`, 400);
143
+ }
144
+ patch.speech = { providerId };
145
+ }
146
+ if (body.previewServers !== undefined) {
147
+ const sanitized = sanitizePreviewServerSettings(body.previewServers);
148
+ const raw = body.previewServers && typeof body.previewServers === "object" && !Array.isArray(body.previewServers)
149
+ ? body.previewServers
150
+ : {};
151
+ if (raw.maxRunningServers !== sanitized.maxRunningServers || raw.autoStopMinutes !== sanitized.autoStopMinutes) {
152
+ throw new PiboWebHttpError("Invalid Preview server settings", 400);
153
+ }
154
+ patch.previewServers = sanitized;
155
+ }
131
156
  if (body.telemetryRetention !== undefined) {
132
157
  const current = loadPiboUserSettings().telemetryRetention;
133
158
  patch.telemetryRetention = {
@@ -0,0 +1,100 @@
1
+ import { loadPiboUserSettings } from "../../core/user-settings.js";
2
+ import { PiboSpeechError, } from "../../speech/types.js";
3
+ import { PiboWebHttpError, readJsonBody, responseJson } from "../../web/http.js";
4
+ export const CHAT_SPEECH_MAX_CHARACTERS = 32_000;
5
+ export const CHAT_SPEECH_MAX_OFFER_CHARACTERS = 256_000;
6
+ export async function responseChatSpeechProviders(context) {
7
+ const providers = await requireProviderInfos(context);
8
+ return responseJson({
9
+ providers,
10
+ selectedProviderId: loadPiboUserSettings().speech.providerId,
11
+ });
12
+ }
13
+ export async function responseChatSpeechSessionStart(request, context) {
14
+ const startSpeechSession = context.channelContext.startSpeechSession;
15
+ if (!startSpeechSession)
16
+ throw new PiboWebHttpError("Speech synthesis is not available", 503);
17
+ const providerId = loadPiboUserSettings().speech.providerId;
18
+ const providers = await requireProviderInfos(context);
19
+ if (!providers.some((provider) => provider.id === providerId)) {
20
+ throw new PiboWebHttpError(`Speech provider "${providerId}" is not registered`, 409);
21
+ }
22
+ const body = await readJsonBody(request);
23
+ if (typeof body.offerSdp !== "string" || !body.offerSdp.startsWith("v=0")) {
24
+ throw new PiboWebHttpError("A WebRTC audio offer is required", 400);
25
+ }
26
+ if (body.offerSdp.length > CHAT_SPEECH_MAX_OFFER_CHARACTERS) {
27
+ throw new PiboWebHttpError("The WebRTC audio offer is too large", 413);
28
+ }
29
+ const text = requireSpeechText(body.text);
30
+ let speechSession;
31
+ try {
32
+ speechSession = await startSpeechSession(providerId, { offerSdp: body.offerSdp, text }, { signal: request.signal });
33
+ if (request.signal.aborted)
34
+ throw request.signal.reason;
35
+ return responseJson({ speechSession }, { status: 201 });
36
+ }
37
+ catch (error) {
38
+ if (speechSession)
39
+ await context.channelContext.stopSpeechSession?.(speechSession.sessionId).catch(() => { });
40
+ throw speechHttpError(error, "Speech session failed to start");
41
+ }
42
+ }
43
+ export async function responseChatSpeechSessionSpeak(request, context, sessionId) {
44
+ const speakSpeechSession = context.channelContext.speakSpeechSession;
45
+ if (!speakSpeechSession)
46
+ throw new PiboWebHttpError("Speech synthesis is not available", 503);
47
+ const body = await readJsonBody(request);
48
+ const text = requireSpeechText(body.text);
49
+ try {
50
+ await speakSpeechSession(sessionId, { text });
51
+ return new Response(null, { status: 204 });
52
+ }
53
+ catch (error) {
54
+ throw speechHttpError(error, "Speech synthesis failed");
55
+ }
56
+ }
57
+ export async function responseChatSpeechSessionStop(context, sessionId) {
58
+ const stopSpeechSession = context.channelContext.stopSpeechSession;
59
+ if (!stopSpeechSession)
60
+ throw new PiboWebHttpError("Speech synthesis is not available", 503);
61
+ try {
62
+ await stopSpeechSession(sessionId);
63
+ return new Response(null, { status: 204 });
64
+ }
65
+ catch (error) {
66
+ throw speechHttpError(error, "Speech session could not be stopped");
67
+ }
68
+ }
69
+ function requireSpeechText(value) {
70
+ if (typeof value !== "string" || !value.trim()) {
71
+ throw new PiboWebHttpError("Speech text is required", 400);
72
+ }
73
+ const text = value.trim();
74
+ if (text.length > CHAT_SPEECH_MAX_CHARACTERS) {
75
+ throw new PiboWebHttpError(`Speech text exceeds the ${CHAT_SPEECH_MAX_CHARACTERS.toLocaleString("en-US")} character limit`, 413);
76
+ }
77
+ return text;
78
+ }
79
+ async function requireProviderInfos(context) {
80
+ const getProviderInfos = context.channelContext.getSpeechProviderInfos;
81
+ if (!getProviderInfos)
82
+ throw new PiboWebHttpError("Speech providers are not available", 503);
83
+ return await getProviderInfos();
84
+ }
85
+ function speechHttpError(error, fallback) {
86
+ if (error instanceof PiboSpeechError) {
87
+ if (error.code === "invalid_offer" || error.code === "invalid_text")
88
+ return new PiboWebHttpError(error.message, 400);
89
+ if (error.code === "session_not_found")
90
+ return new PiboWebHttpError(error.message, 404);
91
+ if (error.code === "not_configured")
92
+ return new PiboWebHttpError(error.message, 409);
93
+ if (error.code === "capacity_exceeded")
94
+ return new PiboWebHttpError(error.message, 429);
95
+ if (error.code === "aborted")
96
+ return new PiboWebHttpError(error.message, 499);
97
+ return new PiboWebHttpError(error.message, 502);
98
+ }
99
+ return new PiboWebHttpError(error instanceof Error ? error.message : fallback, 502);
100
+ }
@@ -1,11 +1,22 @@
1
1
  import { normalizeSessionErrorDetails } from "../../../core/session-errors.js";
2
2
  import { roomWorkspaceFromMetadata } from "../types/rooms.js";
3
+ import { tracePayloadRefForStoredPayload } from "../trace-v2.js";
4
+ const MAX_TRACE_EVENT_HYDRATION_BYTES = 1024 * 1024;
3
5
  export function storedPiboEventFromV2Row(row, payloadStore) {
4
6
  const attributes = parseJsonObject(row.attributes_json);
5
7
  const payload = outputPayloadFromV2Row(row, attributes, readPersistedPayload(row, payloadStore));
6
8
  if (!payload)
7
9
  return undefined;
8
- return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, eventId: row.event_id ?? undefined, streamId: row.stream_id, type: row.type, createdAt: row.created_at, payload };
10
+ const payloadIdentity = tracePayloadIdentityForEvent(row, attributes);
11
+ const storedPayloadRef = row.payload_ref && row.session_id && payloadStore && payloadIdentity
12
+ ? tracePayloadRefForStoredPayload({
13
+ payloadStore,
14
+ piboSessionId: row.session_id,
15
+ payloadId: row.payload_ref,
16
+ ...payloadIdentity,
17
+ })
18
+ : undefined;
19
+ return { id: String(row.stream_id), piboSessionId: row.session_id ?? undefined, eventSequence: row.session_sequence ?? undefined, eventId: row.event_id ?? undefined, streamId: row.stream_id, storedPayloadRef, type: row.type, createdAt: row.created_at, payload };
9
20
  }
10
21
  export function storedChatEventFromV2Row(row, payloadStore) {
11
22
  const attributes = parseJsonObject(row.attributes_json);
@@ -76,16 +87,25 @@ function readPersistedPayload(row, payloadStore) {
76
87
  return undefined;
77
88
  try {
78
89
  const metadata = payloadStore.getPayload(row.payload_ref);
79
- if (!metadata)
90
+ if (!metadata || metadata.byteSize > MAX_TRACE_EVENT_HYDRATION_BYTES)
80
91
  return undefined;
81
- return metadata.contentType.includes("json")
82
- ? payloadStore.readPayloadJson(row.payload_ref)
83
- : payloadStore.readPayloadText(row.payload_ref);
92
+ const text = Buffer.from(payloadStore.readPayloadBytesBounded(row.payload_ref, MAX_TRACE_EVENT_HYDRATION_BYTES)).toString("utf8");
93
+ return metadata.contentType.includes("json") ? JSON.parse(text) : text;
84
94
  }
85
95
  catch {
86
96
  return undefined;
87
97
  }
88
98
  }
99
+ function tracePayloadIdentityForEvent(row, attributes) {
100
+ const toolCallId = stringAttribute(attributes, "toolCallId");
101
+ if (!toolCallId)
102
+ return undefined;
103
+ if (row.type === "tool_call" || row.type === "tool_execution_started")
104
+ return { nodeId: `tool:${toolCallId}`, payloadKind: "input" };
105
+ if (row.type === "tool_execution_updated" || row.type === "tool_execution_finished")
106
+ return { nodeId: `tool:${toolCallId}`, payloadKind: "output" };
107
+ return undefined;
108
+ }
89
109
  export function sessionFromRow(row) { return { piboSessionId: row.id, piSessionId: row.pi_session_id ?? "", runtimeInstanceId: row.runtime_instance_id ?? undefined, runtimeAdapterId: row.runtime_adapter_id ?? undefined, runtimeBindingState: isRuntimeBindingState(row.binding_state) ? row.binding_state : undefined, nativeSessionId: row.native_session_id ?? undefined, parentId: row.parent_id ?? undefined, profile: row.profile, channel: row.channel, kind: row.kind, createdAt: row.created_at, updatedAt: row.updated_at, lastActivityAt: row.last_activity_at, status: row.status === "running" || row.status === "error" ? row.status : "idle" }; }
90
110
  export function roomFromRow(row) { const metadata = parseJsonObject(row.metadata_json); return { id: row.id, name: row.name, topic: row.topic ?? undefined, workspace: row.workspace ?? roomWorkspaceFromMetadata(metadata), type: row.type, parentRoomId: row.parent_room_id ?? undefined, createdAt: row.created_at, updatedAt: row.updated_at, retentionPolicyId: row.retention_policy_id ?? undefined, metadata }; }
91
111
  export function parseJsonObject(value) { try {
@@ -1,5 +1,8 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { messageTurnTimingsFromEvents } from "../../../shared/trace-event-projection.js";
2
3
  import { storedChatEventFromV2Row, storedPiboEventFromV2Row } from "./chat-data-mappers.js";
4
+ import { TRACE_RECONCILIATION_ENTRY_CAP, TRACE_RECONCILIATION_TIMING_CAP } from "../../../shared/trace-limits.js";
5
+ import { parseTraceToolNodeIdentity } from "../../../shared/trace-tool-identity.js";
3
6
  export class ChatTimelineQueryService {
4
7
  store;
5
8
  constructor(store) {
@@ -37,7 +40,10 @@ export class ChatTimelineQueryService {
37
40
  WHERE session_id = ?
38
41
  AND type IN ('message_queued', 'message_steered', 'message_started', 'message_finished', 'session_error', 'thinking_finished', 'assistant_message')
39
42
  ORDER BY session_sequence ASC, stream_id ASC
40
- `).all(piboSessionId);
43
+ LIMIT ?
44
+ `).all(piboSessionId, TRACE_RECONCILIATION_TIMING_CAP + 1);
45
+ if (rows.length > TRACE_RECONCILIATION_TIMING_CAP)
46
+ return [];
41
47
  const events = rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads)).filter((event) => event !== undefined);
42
48
  return messageTurnTimingsFromEvents(events);
43
49
  }
@@ -70,6 +76,67 @@ export class ChatTimelineQueryService {
70
76
  `).all(...values, limit);
71
77
  return rows.map((row) => storedPiboEventFromV2Row(row, this.store.payloads)).filter((event) => event !== undefined);
72
78
  }
79
+ isPayloadAttachedToTraceNode(input) {
80
+ const nodeIdentity = parseTraceToolNodeIdentity(input.nodeId);
81
+ if (!nodeIdentity)
82
+ return false;
83
+ const payload = this.store.payloads.getPayload(input.payloadId);
84
+ if (!payload)
85
+ return false;
86
+ const eventClause = nodeIdentity.qualifier ? "AND event_id = ?" : "";
87
+ const values = nodeIdentity.qualifier
88
+ ? [input.piboSessionId, nodeIdentity.toolCallId, nodeIdentity.qualifier.eventId]
89
+ : [input.piboSessionId, nodeIdentity.toolCallId];
90
+ const rows = this.store.db.prepare(`
91
+ SELECT type, event_id, payload_ref, attributes_json
92
+ FROM event_log
93
+ WHERE session_id = ?
94
+ AND tool_call_id = ?
95
+ AND type IN ('tool_execution_started', 'tool_execution_updated', 'tool_execution_finished')
96
+ ${eventClause}
97
+ ORDER BY session_sequence ASC, stream_id ASC
98
+ LIMIT ?
99
+ `).all(...values, TRACE_RECONCILIATION_ENTRY_CAP + 1);
100
+ if (rows.length > TRACE_RECONCILIATION_ENTRY_CAP)
101
+ return false;
102
+ const lifecycles = new Map();
103
+ const invocations = [];
104
+ for (const row of rows) {
105
+ const lifecycle = lifecycles.get(row.event_id) ?? { nextOrdinal: 0 };
106
+ lifecycles.set(row.event_id, lifecycle);
107
+ if (row.type === "tool_execution_started") {
108
+ if (lifecycle.active)
109
+ return false;
110
+ lifecycle.active = {
111
+ eventId: row.event_id,
112
+ invocationOrdinal: lifecycle.nextOrdinal,
113
+ payloadMatches: false,
114
+ };
115
+ lifecycle.nextOrdinal += 1;
116
+ invocations.push(lifecycle.active);
117
+ }
118
+ else if (!lifecycle.active) {
119
+ lifecycle.active = {
120
+ eventId: row.event_id,
121
+ invocationOrdinal: lifecycle.nextOrdinal,
122
+ payloadMatches: false,
123
+ };
124
+ lifecycle.nextOrdinal += 1;
125
+ invocations.push(lifecycle.active);
126
+ }
127
+ if (row.type !== "tool_execution_started"
128
+ && payloadMatchesEventRow(this.store, payload.sha256, input.payloadId, row))
129
+ lifecycle.active.payloadMatches = true;
130
+ if (row.type === "tool_execution_finished")
131
+ lifecycle.active = undefined;
132
+ }
133
+ if (!nodeIdentity.qualifier) {
134
+ return invocations.length === 1 && invocations[0].payloadMatches;
135
+ }
136
+ const exact = invocations.filter((invocation) => invocation.eventId === nodeIdentity.qualifier.eventId
137
+ && invocation.invocationOrdinal === nodeIdentity.qualifier.invocationOrdinal);
138
+ return exact.length === 1 && exact[0].payloadMatches;
139
+ }
73
140
  countEventsByType(input = {}) {
74
141
  const clauses = [];
75
142
  const values = [];
@@ -111,3 +178,24 @@ export class ChatTimelineQueryService {
111
178
  return row?.stream_id ?? undefined;
112
179
  }
113
180
  }
181
+ function payloadMatchesEventRow(store, payloadSha256, payloadId, row) {
182
+ if (row.payload_ref === payloadId)
183
+ return true;
184
+ let attributes;
185
+ try {
186
+ const parsed = JSON.parse(row.attributes_json);
187
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
188
+ return false;
189
+ attributes = parsed;
190
+ }
191
+ catch {
192
+ return false;
193
+ }
194
+ if (!("inlinePayload" in attributes))
195
+ return false;
196
+ const bytes = Buffer.from(JSON.stringify(attributes.inlinePayload), "utf8");
197
+ const inlineSha256 = createHash("sha256").update(bytes).digest("hex");
198
+ if (payloadSha256 !== inlineSha256)
199
+ return false;
200
+ return store.payloads.findBySha256(inlineSha256)?.id === payloadId;
201
+ }