@shpitdev/convex-cliproxy-observability 0.3.6 → 0.3.7

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 (40) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -2
  3. package/dist/capture/content-health.d.ts +4 -1
  4. package/dist/capture/content-health.d.ts.map +1 -1
  5. package/dist/capture/content-health.js +6 -5
  6. package/dist/capture/content-health.js.map +1 -1
  7. package/dist/capture/index.d.ts +21 -0
  8. package/dist/capture/index.d.ts.map +1 -1
  9. package/dist/capture/index.js +53 -0
  10. package/dist/capture/index.js.map +1 -1
  11. package/dist/component/_generated/api.d.ts.map +1 -1
  12. package/dist/component/_generated/api.js.map +1 -1
  13. package/dist/component/_generated/component.d.ts.map +1 -1
  14. package/dist/component/ingest.d.ts +1 -0
  15. package/dist/component/ingest.d.ts.map +1 -1
  16. package/dist/projection.d.ts +6 -3
  17. package/dist/projection.d.ts.map +1 -1
  18. package/dist/projection.js +55 -7
  19. package/dist/projection.js.map +1 -1
  20. package/dist/protocols/body-parts.d.ts +19 -0
  21. package/dist/protocols/body-parts.d.ts.map +1 -0
  22. package/dist/protocols/body-parts.js +56 -0
  23. package/dist/protocols/body-parts.js.map +1 -0
  24. package/dist/protocols/checkpoint.d.ts +16 -1
  25. package/dist/protocols/checkpoint.d.ts.map +1 -1
  26. package/dist/protocols/checkpoint.js +48 -14
  27. package/dist/protocols/checkpoint.js.map +1 -1
  28. package/dist/protocols/index.d.ts +1 -0
  29. package/dist/protocols/index.d.ts.map +1 -1
  30. package/dist/protocols/index.js +1 -0
  31. package/dist/protocols/index.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/capture/content-health.ts +8 -5
  34. package/src/capture/index.ts +74 -0
  35. package/src/component/_generated/api.ts +5 -6
  36. package/src/component/_generated/component.ts +6 -6
  37. package/src/projection.ts +71 -16
  38. package/src/protocols/body-parts.ts +58 -0
  39. package/src/protocols/checkpoint.ts +51 -13
  40. package/src/protocols/index.ts +1 -0
@@ -1,4 +1,4 @@
1
- import { expandStockHookBlock } from "./stock-block.js";
1
+ import { stockHookPieces } from "./body-parts.js";
2
2
  import {
3
3
  providerIdentity,
4
4
  TOKEN_FIELDS,
@@ -7,7 +7,7 @@ import {
7
7
  type PrivateContentReference,
8
8
  } from "../model-call/index.js";
9
9
  import type { CaptureObservationV1 } from "../capture/index.js";
10
- import { decodeBody } from "../capture/index.js";
10
+ import { continuesBodyPart, decodeBody } from "../capture/index.js";
11
11
  import { assembleResponsesStream } from "./responses.js";
12
12
  import { SSEReader, type SSECheckpoint } from "./framing.js";
13
13
  import { count, list, nanoTime, object, parseObject } from "./values.js";
@@ -28,6 +28,10 @@ export type ProjectionCheckpoint = {
28
28
  streamed?: boolean;
29
29
  inputSeen?: boolean;
30
30
  chatFinished?: boolean;
31
+ /** First segment of an unfinished split body; projection reassembles from there. */
32
+ bodyPartSegment?: number;
33
+ /** Last part of an unfinished split body; the next observation must continue it. */
34
+ openBodyPart?: Pick<CaptureObservationV1, "kind" | "sequence" | "bodyPart">;
31
35
  };
32
36
  const usageFields = [
33
37
  "input_tokens",
@@ -186,10 +190,22 @@ function observeProtocol(
186
190
  }
187
191
  }
188
192
  }
193
+ /**
194
+ * Split bodies up to this size are reassembled for scalar projection. Larger ones
195
+ * stay complete in raw segments; projection does not allocate them in a 64 MiB
196
+ * Convex action.
197
+ */
198
+ export const MAX_PROJECTED_SPLIT_BYTES = 8 * 1024 * 1024;
199
+ /**
200
+ * Earlier parts of a split body only advance the sequence. The final part needs
201
+ * the exact reassembled body (`BodyPartAssembler`) as `assembled`, or `null` when
202
+ * the body exceeds `MAX_PROJECTED_SPLIT_BYTES` and its semantics are not projected.
203
+ */
189
204
  export function applyObservation(
190
205
  call: ModelCallV1,
191
206
  state: ProjectionCheckpoint,
192
207
  o: CaptureObservationV1,
208
+ assembled?: Uint8Array | null,
193
209
  ): void {
194
210
  if (o.sequence !== call.capture.projectedThroughSequence + 1)
195
211
  throw new Error("projection sequence gap");
@@ -204,7 +220,25 @@ export function applyObservation(
204
220
  if (!call.capture.gaps.includes(o.gap) && call.capture.gaps.length < 8)
205
221
  call.capture.gaps.push(o.gap.slice(0, 128));
206
222
  }
207
- const body = decodeBody(o);
223
+ const part = o.bodyPart;
224
+ // Segments validate only their own neighbors; continuity across them is checked here.
225
+ if (
226
+ (state.openBodyPart || (part && part.index > 0)) &&
227
+ !(state.openBodyPart && continuesBodyPart(state.openBodyPart, o))
228
+ )
229
+ throw new Error("body part interrupted");
230
+ const deferred = part !== undefined && part.index < part.count - 1;
231
+ state.openBodyPart = deferred
232
+ ? { kind: o.kind, sequence: o.sequence, bodyPart: part }
233
+ : undefined;
234
+ if (part && !deferred && assembled === undefined)
235
+ throw new Error("split body requires reassembly");
236
+ const unprojected = part !== undefined && !deferred && assembled === null;
237
+ if (unprojected && part.totalBytes <= MAX_PROJECTED_SPLIT_BYTES)
238
+ throw new Error("split body within projection bound");
239
+ // Like an over-limit SSE frame: content is retained, its semantics are not decoded.
240
+ if (unprojected && o.kind !== "request") state.invalid = state.frameLimit = true;
241
+ const body = deferred || unprojected ? new Uint8Array() : (assembled ?? decodeBody(o));
208
242
  const decoder = new TextDecoder("utf-8", { fatal: true });
209
243
  // A new after-auth event replaces the selection, including unknown fields.
210
244
  // Stream initialization may omit auth; an explicit selection replaces the pair.
@@ -227,14 +261,21 @@ export function applyObservation(
227
261
  call.correlationConflicts = o.correlationConflicts ?? [];
228
262
  call.traceId = o.correlation?.traceId;
229
263
  call.sourceTraceId = o.sourceTraceId;
230
- try {
231
- const input = parseObject(decoder.decode(body));
232
- state.streamed = input.stream === true;
233
- call.streamed = state.streamed;
234
- } catch {
235
- state.invalid = true;
264
+ if (!deferred && !unprojected) {
265
+ try {
266
+ const input = parseObject(decoder.decode(body));
267
+ state.streamed = input.stream === true;
268
+ call.streamed = state.streamed;
269
+ } catch {
270
+ state.invalid = true;
271
+ }
236
272
  }
237
273
  }
274
+ // An unparsed request (e.g. a split body over the projection bound) takes
275
+ // streaming from the protocol evidence that follows it.
276
+ if (call.streamed === undefined && (o.kind === "stream_init" || o.kind === "stream_chunk"))
277
+ call.streamed = true;
278
+ else if (call.streamed === undefined && o.kind === "response") call.streamed = false;
238
279
  // Derived read fields: reads fill them only for summaries projected before 0.3.0.
239
280
  Object.assign(call, providerIdentity(call), { costProvenance: call.cost.kind });
240
281
  if (o.kind === "response" || o.kind === "stream_chunk" || o.kind === "completion") {
@@ -251,10 +292,7 @@ export function applyObservation(
251
292
  }
252
293
  if ((o.kind === "stream_chunk" || o.kind === "completion") && body.length) {
253
294
  const reader = new SSEReader(state.sse);
254
- const pieces =
255
- o.bodyFraming === "stock_hook_block"
256
- ? expandStockHookBlock(body, o.stockHookChunkLengths ?? [])
257
- : [body];
295
+ const pieces = o.bodyFraming === "stock_hook_block" ? stockHookPieces(o, body) : [body];
258
296
  const frames = pieces.flatMap((piece) =>
259
297
  o.bodyFraming === "stock_hook_chunk" || o.bodyFraming === "stock_hook_block"
260
298
  ? reader.feedStock(piece, call.clientProtocol, o.observedAt)
@@ -144,3 +144,4 @@ export function projectCapturedPayloads(args: {
144
144
  }
145
145
 
146
146
  export { expandStockHookBlock } from "./stock-block.js";
147
+ export { BodyPartAssembler, stockHookPieces } from "./body-parts.js";