@sogni-ai/sogni-client 5.65.0 → 5.66.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.
@@ -4,6 +4,12 @@ import { MessageDeliveryUncertainError, SEND_READY_TIMEOUT_MS } from '../request
4
4
 
5
5
  const PRIMARY_HEARTBEAT_INTERVAL = 2000;
6
6
  const PRIMARY_TIMEOUT = 4000;
7
+ // How long a visible tab waits without a heartbeat from the primary before it
8
+ // takes the connection over itself. A primary in a background tab can keep
9
+ // answering elections (messages are not throttled) while its timers, and so its
10
+ // heartbeats, have all but stopped; requests forwarded to it can then wait on a
11
+ // connection that no longer works.
12
+ const PRIMARY_TAKEOVER_TIMEOUT = 12000;
7
13
  const ELECTION_SETTLE_TIMEOUT = 600;
8
14
  const ACK_TIMEOUT = 5000;
9
15
 
@@ -126,6 +132,11 @@ class ChannelCoordinator<M, N> {
126
132
 
127
133
  // Heartbeat to detect primary tab death
128
134
  private lastPrimaryHeartbeat: number = Date.now();
135
+ // The primary this tab follows, and when it last sent a heartbeat of its own.
136
+ // Unlike lastPrimaryHeartbeat, the primary re-announcing itself in answer to
137
+ // an election does not count.
138
+ private primaryId: string | null = null;
139
+ private lastHeartbeatFromPrimary: number = Date.now();
129
140
  private heartbeatTimer: NodeJS.Timeout | null = null;
130
141
  private primaryCheckTimer: NodeJS.Timeout | null = null;
131
142
  private readyCallback: () => void | null = () => {};
@@ -160,6 +171,10 @@ class ChannelCoordinator<M, N> {
160
171
  return this._isPrimary;
161
172
  }
162
173
 
174
+ private tabIsVisible() {
175
+ return isActiveTab;
176
+ }
177
+
163
178
  private get currentPriority() {
164
179
  return isActiveTab ? this.priority * 10 : this.priority;
165
180
  }
@@ -209,8 +224,24 @@ class ChannelCoordinator<M, N> {
209
224
  return new Promise((resolve) => setTimeout(resolve, ELECTION_SETTLE_TIMEOUT));
210
225
  }
211
226
 
227
+ /**
228
+ * A visible secondary whose primary has sent no heartbeat for
229
+ * PRIMARY_TAKEOVER_TIMEOUT becomes primary itself. Announcing demotes the
230
+ * silent primary, which closes its connection; this tab opens its own.
231
+ */
232
+ private takeOverFromSilentPrimary(): boolean {
233
+ if (this._isPrimary || !this.tabIsVisible()) return false;
234
+ const silentFor = Date.now() - this.lastHeartbeatFromPrimary;
235
+ if (silentFor <= PRIMARY_TAKEOVER_TIMEOUT) return false;
236
+ this.logger.debug(`Primary sent no heartbeat for ${silentFor}ms, taking over as primary`);
237
+ this.electionInProgress = false;
238
+ this.electionResponses.clear();
239
+ this.becomePrimary();
240
+ return true;
241
+ }
242
+
212
243
  private async ensureFreshPrimaryBeforeRequest(): Promise<void> {
213
- if (this._isPrimary) {
244
+ if (this._isPrimary || this.takeOverFromSilentPrimary()) {
214
245
  return;
215
246
  }
216
247
 
@@ -260,6 +291,7 @@ class ChannelCoordinator<M, N> {
260
291
 
261
292
  private becomePrimary() {
262
293
  this._isPrimary = true;
294
+ this.primaryId = this.id;
263
295
  this.lastPrimaryHeartbeat = Date.now();
264
296
  this.broadcast({
265
297
  type: MessageType.PRIMARY_ANNOUNCE,
@@ -300,6 +332,7 @@ class ChannelCoordinator<M, N> {
300
332
  }
301
333
  this.primaryCheckTimer = setInterval(() => {
302
334
  if (!this._isPrimary) {
335
+ if (this.takeOverFromSilentPrimary()) return;
303
336
  const timeSinceLastHeartbeat = Date.now() - this.lastPrimaryHeartbeat;
304
337
  if (timeSinceLastHeartbeat > PRIMARY_TIMEOUT) {
305
338
  this.startElections();
@@ -325,7 +358,7 @@ class ChannelCoordinator<M, N> {
325
358
  case MessageType.PRIMARY_ANNOUNCE:
326
359
  return this.handlePrimaryAnnounce(message);
327
360
  case MessageType.PRIMARY_HEARTBEAT:
328
- return this.handlePrimaryHeartbeat();
361
+ return this.handlePrimaryHeartbeat(message);
329
362
  case MessageType.BROADCAST:
330
363
  return this.handleBroadcast(message);
331
364
  case MessageType.REQUEST:
@@ -373,6 +406,12 @@ class ChannelCoordinator<M, N> {
373
406
  const wasPrimary = this._isPrimary;
374
407
  this._isPrimary = false;
375
408
  this.lastPrimaryHeartbeat = Date.now();
409
+ // A new primary starts with a clean record; the same primary answering an
410
+ // election again proves only that its tab still handles messages.
411
+ if (message.payload.id !== this.primaryId) {
412
+ this.primaryId = message.payload.id;
413
+ this.lastHeartbeatFromPrimary = Date.now();
414
+ }
376
415
  this.electionInProgress = false;
377
416
  this.electionResponses.clear();
378
417
 
@@ -386,8 +425,10 @@ class ChannelCoordinator<M, N> {
386
425
  this.readyCallback();
387
426
  }
388
427
 
389
- private handlePrimaryHeartbeat() {
428
+ private handlePrimaryHeartbeat(message: PrimaryHeartbeat) {
390
429
  this.lastPrimaryHeartbeat = Date.now();
430
+ this.primaryId = message.payload.id;
431
+ this.lastHeartbeatFromPrimary = Date.now();
391
432
  }
392
433
 
393
434
  private handleBroadcast(message: BroadcastMessage) {
package/src/Chat/index.ts CHANGED
@@ -1220,11 +1220,14 @@ class ChatApi extends ApiGroup<ChatApiEvents> {
1220
1220
  }
1221
1221
 
1222
1222
  private handleSocketAuthenticated(data: AuthenticatedData): void {
1223
- if (!this.jobsAwaitingReconnect.size || !Array.isArray(data?.activeLLMJobIDs)) return;
1223
+ if (!Array.isArray(data?.activeLLMJobIDs)) return;
1224
1224
  const live = new Set(data.activeLLMJobIDs.map((id) => String(id).toUpperCase()));
1225
+ // Every job already sent, not only those a 'connecting' put on watch: a tab
1226
+ // that takes the connection over from another tab gets no 'connecting', and
1227
+ // a job queued on the old connection was cancelled when it closed.
1225
1228
  const gone: string[] = [];
1226
- for (const jobID of this.jobsAwaitingReconnect) {
1227
- if (!live.has(jobID.toUpperCase())) gone.push(jobID);
1229
+ for (const jobID of this.activeStreams.keys()) {
1230
+ if (!this.unsentJobs.has(jobID) && !live.has(jobID.toUpperCase())) gone.push(jobID);
1228
1231
  }
1229
1232
  this.jobsAwaitingReconnect.clear();
1230
1233
  this.clearTransportGraceTimer();
@@ -1372,15 +1372,32 @@ function applyImageParams(
1372
1372
  params: ImageProjectParams,
1373
1373
  options: ImageModelOptions
1374
1374
  ) {
1375
- if (['moss_image_sft_int8_convrot', 'moss_image_sft_dmd8_int8_convrot'].includes(params.modelId) &&
1376
- (params.startingImage || params.contextImages?.length)) {
1377
- throw new Error('MOSS-Image SFT generates from text and does not accept guide or reference images.');
1375
+ if (
1376
+ ['moss_image_sft_int8_convrot', 'moss_image_sft_dmd8_int8_convrot'].includes(params.modelId) &&
1377
+ (params.startingImage || params.contextImages?.length)
1378
+ ) {
1379
+ throw new Error(
1380
+ 'MOSS-Image SFT generates from text and does not accept guide or reference images.'
1381
+ );
1378
1382
  }
1379
1383
  const keyFrame: Record<string, any> = {
1380
1384
  ...inputKeyframe,
1381
1385
  sizePreset: params.sizePreset
1382
1386
  };
1383
1387
  const contextImages = params.contextImages || [];
1388
+ for (const name of ['mossRopeScaleX', 'mossRopeScaleY'] as const) {
1389
+ const value = params[name];
1390
+ if (value === undefined) continue;
1391
+ if (
1392
+ !['moss_image_sft_int8_convrot', 'moss_image_sft_dmd8_int8_convrot'].includes(params.modelId)
1393
+ ) {
1394
+ throw new Error(`${name} is only supported by MOSS-Image`);
1395
+ }
1396
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 100) {
1397
+ throw new Error(`${name} must be a finite number between 0 and 100`);
1398
+ }
1399
+ if (value !== 1) keyFrame[name] = value;
1400
+ }
1384
1401
  for (let index = 1; index <= 16; index += 1) {
1385
1402
  keyFrame[`hasContextImage${index}`] = !!contextImages[index - 1];
1386
1403
  }
@@ -931,6 +931,10 @@ export interface Sam3Selection {
931
931
  export interface ImageProjectParams
932
932
  extends BaseProjectParams, Pixal3dGenerationOptions, Pixal3dMultiViewImages {
933
933
  type: 'image';
934
+ /** MOSS only: horizontal spatial scaling, 0–100; 1 is neutral. Changes composition, not output size. */
935
+ mossRopeScaleX?: number;
936
+ /** MOSS only: vertical spatial scaling, 0–100; 1 is neutral. */
937
+ mossRopeScaleY?: number;
934
938
  /**
935
939
  * Number of previews to generate. Note that previews affect project cost
936
940
  */
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED by scripts/generate-version.mjs — do not edit.
2
2
  // Source of truth: package.json version.
3
- export const LIB_VERSION = '5.65.0';
3
+ export const LIB_VERSION = '5.66.0';