@steve31415/baselib 3.33.0 → 3.33.2

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.
@@ -15,21 +15,47 @@ function classifyFinish(finishReason) {
15
15
  return 'complete';
16
16
  return 'other';
17
17
  }
18
- /** Recursively strip fields that Gemini's API doesn't support (e.g. additionalProperties). */
19
- function stripUnsupportedSchemaFields(schema) {
20
- if (Array.isArray(schema)) {
21
- return schema.map(stripUnsupportedSchemaFields);
22
- }
23
- if (schema !== null && typeof schema === 'object') {
24
- const result = {};
25
- for (const [key, value] of Object.entries(schema)) {
26
- if (key === 'additionalProperties')
27
- continue;
28
- result[key] = stripUnsupportedSchemaFields(value);
18
+ /**
19
+ * Transform a portable JSON schema into the dialect Gemini's `responseSchema`
20
+ * accepts — an OpenAPI-subset proto, not full JSON Schema:
21
+ * - `additionalProperties` is dropped (Gemini rejects it).
22
+ * - A `type` array (JSON Schema's union; usually the nullable union
23
+ * `['string', 'null']`) becomes one `type`, or an `anyOf` of single types
24
+ * for a multi-type union, plus `nullable: true` when `'null'` was a member.
25
+ * The proto's `type` holds a single value, so an array is rejected with a
26
+ * 400 before the model runs ("Proto field is not repeating, cannot start
27
+ * list"; verified live on 3.5 Flash-Lite, 3.6 Flash and 3.1 Pro, 2026-10-09).
28
+ * Returns a deep copy — the caller's schema object is reused across failover
29
+ * attempts.
30
+ */
31
+ function toGeminiSchema(schema) {
32
+ if (Array.isArray(schema))
33
+ return schema.map(toGeminiSchema);
34
+ if (schema === null || typeof schema !== 'object')
35
+ return schema;
36
+ const result = {};
37
+ for (const [key, value] of Object.entries(schema)) {
38
+ if (key === 'additionalProperties')
39
+ continue;
40
+ // Only a keyword `type` is ever an array: a property named `type` (an
41
+ // entry of a `properties` map) has a schema object as its value.
42
+ if (key === 'type' && Array.isArray(value)) {
43
+ Object.assign(result, singleGeminiType(value));
44
+ continue;
29
45
  }
30
- return result;
46
+ result[key] = toGeminiSchema(value);
31
47
  }
32
- return schema;
48
+ return result;
49
+ }
50
+ /** A JSON Schema type array as Gemini's single `type` / `anyOf` plus `nullable`. */
51
+ function singleGeminiType(types) {
52
+ const nonNull = types.filter((type) => type !== 'null');
53
+ if (nonNull.length === 0)
54
+ return { type: 'null' };
55
+ const nullable = nonNull.length < types.length ? { nullable: true } : {};
56
+ if (nonNull.length === 1)
57
+ return { type: nonNull[0], ...nullable };
58
+ return { anyOf: nonNull.map((type) => ({ type })), ...nullable };
33
59
  }
34
60
  export const geminiProvider = {
35
61
  name: 'gemini',
@@ -50,7 +76,7 @@ export const geminiProvider = {
50
76
  }
51
77
  if (request.jsonSchema) {
52
78
  generationConfig.responseMimeType = 'application/json';
53
- generationConfig.responseSchema = stripUnsupportedSchemaFields(request.jsonSchema.schema);
79
+ generationConfig.responseSchema = toGeminiSchema(request.jsonSchema.schema);
54
80
  }
55
81
  // Gemini thinking models reason before answering; the reasoning tokens
56
82
  // count against maxOutputTokens. `effort` (pre-gated and clamped in
@@ -38,7 +38,7 @@ function makeNullable(propSchema) {
38
38
  * Outputs mode requires: every object must set `additionalProperties: false`
39
39
  * and list ALL of its properties in `required`. Callers author one portable
40
40
  * schema; this keeps the OpenAI-specific dialect local to this adapter,
41
- * mirroring the Gemini adapter's `stripUnsupportedSchemaFields` and the
41
+ * mirroring the Gemini adapter's `toGeminiSchema` and the
42
42
  * Anthropic adapter's `toAnthropicStrictSchema` (which closes objects but keeps
43
43
  * optional fields optional). Returns a deep copy — the
44
44
  * caller's schema object is never mutated (it is reused across failover attempts).
@@ -217,6 +217,12 @@ export declare class SyncClient<B, C extends EventBody, S extends EventBody, Sum
217
217
  * the page counted as visible is counted (hidden pages are throttled by
218
218
  * design and their time is `hiddenMs`). */
219
219
  private observeStall;
220
+ /** The sleep half of `observeStall`, also run by every event-driven entry
221
+ * that starts, ends or splits the accounting (outage start and end, an
222
+ * `online` event, a hide): a wake can reach the page as one of those
223
+ * events before any timer runs, and the sleep must land on the right side
224
+ * of it (reader, 2026-10-11). */
225
+ private observeClockJump;
220
226
  private setTimer;
221
227
  private setRepeating;
222
228
  private isHidden;
@@ -382,20 +382,27 @@ export class SyncClient {
382
382
  * the page counted as visible is counted (hidden pages are throttled by
383
383
  * design and their time is `hiddenMs`). */
384
384
  observeStall(dueMono) {
385
+ this.observeClockJump();
385
386
  const mono = this.monoNow();
386
- const offset = this.now() - mono;
387
- const visible = !this.isHidden();
388
- if (visible && this.clockOffset !== null && offset - this.clockOffset >= STALL_MIN_MS) {
389
- this.counters.stalledMs += offset - this.clockOffset;
390
- }
391
- this.clockOffset = offset;
392
- if (visible && mono - dueMono >= STALL_MIN_MS) {
387
+ if (!this.isHidden() && mono - dueMono >= STALL_MIN_MS) {
393
388
  const from = Math.max(dueMono, this.stallCoveredUntil, this.visibleSinceMono);
394
389
  if (mono > from)
395
390
  this.counters.stalledMs += mono - from;
396
391
  this.stallCoveredUntil = mono;
397
392
  }
398
393
  }
394
+ /** The sleep half of `observeStall`, also run by every event-driven entry
395
+ * that starts, ends or splits the accounting (outage start and end, an
396
+ * `online` event, a hide): a wake can reach the page as one of those
397
+ * events before any timer runs, and the sleep must land on the right side
398
+ * of it (reader, 2026-10-11). */
399
+ observeClockJump() {
400
+ const offset = this.now() - this.monoNow();
401
+ if (!this.isHidden() && this.clockOffset !== null && offset - this.clockOffset >= STALL_MIN_MS) {
402
+ this.counters.stalledMs += offset - this.clockOffset;
403
+ }
404
+ this.clockOffset = offset;
405
+ }
399
406
  setTimer(callback, delayMs) {
400
407
  const dueMono = this.monoNow() + delayMs;
401
408
  return this.options.runtime.setTimeout(() => {
@@ -439,8 +446,9 @@ export class SyncClient {
439
446
  if (this.halted || this.disposed || this.hiddenSince !== null)
440
447
  return;
441
448
  this.finishReturnWindow();
442
- // A device that slept and then hid the page: the sleep counts as hidden.
443
- this.rebaselineClocks();
449
+ // A device that slept with the page visible and hid it on waking: the
450
+ // sleep counts as stalled, before the hidden spell starts.
451
+ this.observeClockJump();
444
452
  this.hiddenSince = this.now();
445
453
  this.spellCounters = this.snapshot();
446
454
  this.hiddenSocketFailed = false;
@@ -483,6 +491,7 @@ export class SyncClient {
483
491
  onOnline() {
484
492
  if (this.halted || this.disposed)
485
493
  return;
494
+ this.observeClockJump();
486
495
  if (this.isHidden()) {
487
496
  if (this.socket === null) {
488
497
  this.cancelBackgroundTick();
@@ -966,6 +975,10 @@ export class SyncClient {
966
975
  startOutage(closeCode, wasClean = false) {
967
976
  if (this.outageStartedAt !== null)
968
977
  return;
978
+ // A sleep or freeze that ended in this close happened before the outage
979
+ // began (durationMs starts now): keep it out of the outage's stalledMs.
980
+ this.observeClockJump();
981
+ this.stallCoveredUntil = Math.max(this.stallCoveredUntil, this.monoNow());
969
982
  this.outageStartedAt = this.options.runtime.now();
970
983
  this.outageEscalated = false;
971
984
  this.outageCloseCode = closeCode;
@@ -1300,6 +1313,7 @@ export class SyncClient {
1300
1313
  }
1301
1314
  onSyncEstablished() {
1302
1315
  if (this.outageStartedAt !== null) {
1316
+ this.observeClockJump();
1303
1317
  const durationMs = Math.max(0, this.options.runtime.now() - this.outageStartedAt);
1304
1318
  // Failures are only counted (and the ladder only runs) while visible,
1305
1319
  // and the count restarts on each return, so `attempts` is what the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steve31415/baselib",
3
- "version": "3.33.0",
3
+ "version": "3.33.2",
4
4
  "description": "Plasticine new-world shared platform library: logging, auth, service-to-service auth, db, HTTP, sync, app updates",
5
5
  "type": "module",
6
6
  "license": "MIT",