@vibexnpm/talkx 5.2.0 → 5.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibexnpm/talkx",
3
- "version": "5.2.0",
3
+ "version": "5.4.0",
4
4
  "type": "module",
5
5
  "description": "TalkFlow SDK - Chat & WebRTC unified client",
6
6
  "main": "dist/talkflow-sdk.umd.js",
@@ -320,6 +320,21 @@ class ChatClient extends EventEmitter {
320
320
  // 1) WebSocket 구독을 먼저 — 이후 서버 read 이벤트 유실 방지
321
321
  if (!wasAlreadySubscribed) {
322
322
  await this.subscribeRoom(roomId);
323
+ } else if (this.assistantStreamReplay) {
324
+ // ⭐ <b>이미 구독 중인 방으로 재입장</b> — 구독을 유지한 채 방 화면만 나갔다 들어오는 흐름이다
325
+ // (목록 화면에서 unread 를 받으려면 구독을 끊지 않는 것이 정상 구현이다). 이때 subscribeRoom 이
326
+ // 통째로 건너뛰어져 <b>스냅샷 트리거가 하나도 돌지 않는다</b> — 재개 경로는 여기와 재연결
327
+ // (`resumeAssistantStreams`) 둘뿐이기 때문이다.
328
+ //
329
+ // 진행 표시는 transient 이벤트라 화면이 언마운트되면 프론트에서 사라지는데, SDK 슬롯에만 남은
330
+ // 그 상태를 다시 그려줄 신호가 없다 → 다음 PHASE 가 올 때까지(워커 heartbeat 간격 15초) 배지가
331
+ // 안 뜬다. 스냅샷을 다시 받아 그 공백을 없앤다.
332
+ //
333
+ // await 하지 않는다 — 구독은 살아 있으므로 그 사이 도착분은 보류함에 쌓였다가 병합 후 순서대로
334
+ // 풀린다(§6.1). 실패해도 보류분은 반드시 풀리므로 라이브만으로 계속 동작한다.
335
+ const generation = this._armAssistantSnapshot(roomId);
336
+ this._loadAssistantSnapshot(roomId, generation)
337
+ .catch((error) => this.logger.warn(`Assistant snapshot on re-enter failed: room=${roomId}`, error));
323
338
  }
324
339
  // 2) 자동 읽음 처리 활성 방 지정
325
340
  this.setActiveRoom(roomId);
@@ -633,23 +648,24 @@ class ChatClient extends EventEmitter {
633
648
  * @param {Object} [options]
634
649
  * @param {string} [options.format] - 요약 포맷 ({@link SummarizeFormat}). 미지정/알 수 없는 값이면 서버가 {@code SHORT} 적용.
635
650
  * @param {string} [options.lang] - 출력 언어(BCP-47 예 {@code 'ko'}/{@code 'en'}, 또는 {@code 'OFF'}=원문). 미지정 시 요청자 언어로 해소.
636
- * @returns {Promise<{status: string, format?: string, language?: string}>} {@code status}: {@code 'STARTED'}(워커 시작 —
637
- * {@code summaryUpdate} 대기) | {@code 'ALREADY_RUNNING'}(방에서 <b>다른</b> 요약이 진행 중일 수 있음 — 응답의
638
- * {@code format}/{@code language} 로 내 키인지 확인) | {@code 'NO_CHANGE'}(새 대화 없음 — 알림 발생 안 함).
639
- * {@code format}/{@code language}=이번 요청이 해소한 키(방 단위 경합 식별용).
640
- *
641
- * @fires summaryUpdate - { roomId, status: 'READY'|'NO_CHANGE'|'FAILED'|'INVALIDATED', format, languages, updatedAt }.
651
+ * @returns {Promise<{status: string, format?: string, language?: string}>} {@code status}: {@code 'QUEUED'} —
652
+ * 방 단위 durable work item 에 저장·병합됐다. local worker 포화여도 요청은 버려지지 않으며
653
+ * 최종 {@code READY|NO_CHANGE|NO_MESSAGES|FAILED} {@code summaryUpdate} 를 기다린다.
654
+ * {@code format}/{@code language}=이번 호출이 work item 에 병합한 키.
655
+ * ⚠️ 닫힌 목록이 아니다 — 분기에는 default 를 두고, 모르는 값에 스피너를 띄우지 마라.
656
+ * @throws {Error} HTTP 503 {@code SUMMARY_REFRESH_UNAVAILABLE} — durable 저장 자체가 실패했다.
657
+ * 간격을 두고 재시도하고 사용자에게는 일시 오류로 알린다.
658
+ * {@code err.status}=503, {@code err.response.code}={@code 'SUMMARY_REFRESH_UNAVAILABLE'}.
659
+ *
660
+ * @fires summaryUpdate - { roomId, status: 'READY'|'NO_CHANGE'|'NO_MESSAGES'|'FAILED'|'INVALIDATED'|string, format, languages, updatedAt }.
642
661
  * fan-out 으로 여러 언어를 한 번에 생성하므로 {@code languages} 는 배열(§S1). {@code INVALIDATED} 는 refreshSummary 가
643
662
  * 아니라 방 원문 삭제/편집으로 발생(요약이 통째로 폐기됨).
644
663
  *
645
664
  * @example
646
665
  * const { status } = await chat.refreshSummary(roomId, { format: SummarizeFormat.MINUTES, lang: 'ko' });
647
- * if (status === 'STARTED') showSpinner(); // fan-out 진행 시작 — summaryUpdate 대기
648
- * else if (status === 'ALREADY_RUNNING') {
649
- * // 방에서 다른 요약이 도는 중일 수 있다. 완료 이벤트 summaryUpdate 의 languages 에 내 언어가 없으면
650
- * // 그때 다시 refreshSummary 로 내 언어를 요청한다(재요청 금지가 아니라 "내 언어가 없으면 재요청").
651
- * showSpinner();
652
- * } else if (status === 'NO_CHANGE') toast('변경 사항이 없습니다');
666
+ * if (status === 'QUEUED') showQueuedProgress(); // durable 접수 — summaryUpdate 대기
667
+ * else showUnknownSummaryState(status); // 미래 서버 값은 추측하지 않는다
668
+ * // 503(SUMMARY_REFRESH_UNAVAILABLE)은 durable 접수 실패 — try/catch로 받고 간격을 두고 재시도.
653
669
  */
654
670
  async refreshSummary(roomId, options = {}) {
655
671
  const { format, lang } = options;
@@ -663,9 +679,10 @@ class ChatClient extends EventEmitter {
663
679
  /**
664
680
  * Rolling 누적 요약 조회 — 통합본(rollup) + 구간 부분요약(segments) 페이지. 저장된 요약의 단일 진실 소스.
665
681
  *
666
- * <p>{@code summaryUpdate}({@code status:'READY'}) 수신 후, 그리고 화면 진입/재접속 시 호출한다 — 알림은
667
- * transient(저장 안 됨)라 유실될 수 있으므로 GET 으로 동기화한다. 아직 한 번도 요약된 적 없으면 {@code rollup}
668
- * 이 {@code null} 이다. segment 는 {@code afterSeq}/{@code size} 로 페이지네이션한다({@code hasNext}/{@code nextSeq}).</p>
682
+ * <p>terminal {@code summaryUpdate} 수신 후, 화면 진입/재접속 시, 그리고 {@code QUEUED} 뒤 앱이 정한 완료
683
+ * timeout에 <b>한 번</b> 호출한다 — 알림은 transient(저장 안 됨)라 유실될 수 있으므로 GET 으로 동기화한다.
684
+ * 주기적 polling은 하지 않는다. 아직 한 번도 요약된 적 없으면 {@code rollup} 이 {@code null} 이다.
685
+ * segment 는 {@code afterSeq}/{@code size} 로 페이지네이션한다({@code hasNext}/{@code nextSeq}).</p>
669
686
  *
670
687
  * <p>같은 방을 여러 포맷×언어로 보관하므로 {@code options.format}/{@code options.lang} 으로 골라 조회한다.
671
688
  * {@code lang} 미지정 시 서버가 요청자 언어로 해소하고, 응답 {@code language} 로 <b>어느 언어 기준</b>인지
@@ -679,7 +696,7 @@ class ChatClient extends EventEmitter {
679
696
  * @param {string} [options.lang] - 조회할 언어(BCP-47 예 {@code 'ko'}, 또는 {@code 'OFF'}=원문). 미지정 시 요청자 언어로 해소(응답 {@code language}).
680
697
  * @param {number} [options.afterSeq] - segment 페이지 커서(exclusive). 미지정 시 처음부터.
681
698
  * @param {number} [options.size=50] - segment 페이지 크기(서버 상한 50).
682
- * @returns {Promise<RoomSummaryView>} { language, rollup | null, availableFormats, segments, hasNext, nextSeq }
699
+ * @returns {Promise<RoomSummaryView>} { language, roomStatus, rollup | null, availableFormats, segments, hasNext, nextSeq }
683
700
  *
684
701
  * @example
685
702
  * const view = await chat.getSummary(roomId, { format: 'timeline', lang: 'ko' });
@@ -3377,24 +3394,27 @@ class ChatClient extends EventEmitter {
3377
3394
  /**
3378
3395
  * Rolling 누적 요약 완료 알림(summary 채널) 수신 처리 — status 를 그대로 {@code summaryUpdate} 로 emit.
3379
3396
  *
3380
- * <p>서버 {@code SummaryStreamEvent}: {@code { status: 'READY'|'NO_CHANGE'|'FAILED'|'INVALIDATED', roomId, format?, languages?, updatedAt? }}.
3397
+ * <p>서버 {@code SummaryStreamEvent}: {@code { status: 'READY'|'NO_CHANGE'|'NO_MESSAGES'|'FAILED'|'INVALIDATED'|string, roomId, format?, languages?, updatedAt? }}.
3381
3398
  * assistant-stream 과 달리 단계/토큰 누적이 없어 상태 관리 없이 그대로 통지한다. transient(저장 안 됨)라 UI 는
3382
3399
  * 이벤트를 신호로만 쓰고 확정 내용은 {@link #getSummary} 로 읽는다. {@code READY}=요약 갱신됨(→ getSummary),
3383
- * {@code FAILED}=실패(재시도 가능), {@code NO_CHANGE}=변경 없음(대개 refreshSummary 응답에서 동기로 이미 받음),
3400
+ * {@code FAILED}=실패(재시도 가능), {@code NO_CHANGE}=durable 작업이 변경 없이 완료,
3401
+ * {@code NO_MESSAGES}=해당 키에 요약할 대화 없음,
3384
3402
  * {@code INVALIDATED}=방 원문 삭제/편집으로 요약이 통째로 버려짐(→ 요약 화면을 즉시 비우고, 필요 시 refreshSummary 로 재생성).</p>
3385
3403
  *
3386
3404
  * <p><b>키 식별(format+languages)</b>: 한 refresh 가 방 활성 언어들을 fan-out 으로 한 번에 생성하므로(§S1), 완료
3387
- * 이벤트의 {@code languages} 에 내 언어가 있으면 {@code getSummary({ format, lang })} 로 읽는다. {@code READY}·
3388
- * {@code FAILED} 모두 언어 목록을 싣는다({@code NO_CHANGE}·{@code INVALIDATED} 는 없음).</p>
3405
+ * 이벤트의 {@code languages} 에 내 언어가 있으면 {@code getSummary({ format, lang })} 로 읽는다.
3406
+ * {@code READY}·{@code NO_CHANGE}·{@code NO_MESSAGES}·{@code FAILED} 모두 언어 목록을 싣는다
3407
+ * ({@code INVALIDATED} 만 없음). 알 수 없는 미래 status도 그대로 전달하므로 소비자는 default 분기와
3408
+ * one-shot {@code getSummary} 재동기화를 둔다.</p>
3389
3409
  * @private
3390
3410
  */
3391
3411
  _handleSummaryEvent(roomId, event) {
3392
3412
  if (!event || !event.status) return;
3393
3413
  this.emit('summaryUpdate', {
3394
3414
  roomId,
3395
- status: event.status, // 'READY' | 'NO_CHANGE' | 'FAILED' | 'INVALIDATED'
3396
- format: event.format || null, // READY/FAILED 포함(NO_CHANGE·INVALIDATED 는 null)
3397
- languages: event.languages || null, // fan-out 으로 생성/최신화된 언어 목록(READY/FAILED). 그 외 null
3415
+ status: event.status, // known status + future server values (pass-through)
3416
+ format: event.format || null, // READY/NO_CHANGE/NO_MESSAGES/FAILED 포함
3417
+ languages: event.languages || null, // READY/NO_CHANGE/NO_MESSAGES/FAILED 처리 언어 목록
3398
3418
  updatedAt: event.updatedAt || null
3399
3419
  });
3400
3420
  }
@@ -3490,17 +3510,29 @@ class ChatClient extends EventEmitter {
3490
3510
  * @private
3491
3511
  */
3492
3512
  _emitAssistantRoomProgress(roomId) {
3513
+ // 이벤트 계약은 종전 그대로 둔다 — stream 목록은 getter 전용(이벤트로 매 틱 배열을 실어 보내면
3514
+ // DELTA 마다 방 전체 스트림을 다시 만들어 흘리는 셈이다).
3515
+ const { streams, ...event } = this._assistantRoomProgressPayload(roomId);
3516
+ this.emit('assistantRoomProgress', event);
3517
+ }
3518
+
3519
+ /**
3520
+ * 방 단위 집계 payload 생성 — {@link #_emitAssistantRoomProgress} 와 {@link #getAssistantProgress} 의
3521
+ * <b>공통 SSOT</b>. 두 곳에서 각자 조립하면 "이벤트로 본 상태"와 "물어봐서 본 상태"가 갈린다.
3522
+ * @private
3523
+ */
3524
+ _assistantRoomProgressPayload(roomId) {
3493
3525
  const slots = this._assistantProgress.get(roomId);
3494
3526
  const alive = slots ? Array.from(slots.entries()) : [];
3495
3527
  if (alive.length === 0) {
3496
- this.emit('assistantRoomProgress', {
3528
+ return {
3497
3529
  roomId, active: false, streamCount: 0,
3498
- streamId: null, personaId: null, phase: null, label: null, text: null, textAvailable: true
3499
- });
3500
- return;
3530
+ streamId: null, personaId: null, phase: null, label: null, text: null, textAvailable: true,
3531
+ streams: []
3532
+ };
3501
3533
  }
3502
3534
  const [streamId, slot] = alive.reduce((a, b) => (b[1].order > a[1].order ? b : a));
3503
- this.emit('assistantRoomProgress', {
3535
+ return {
3504
3536
  roomId,
3505
3537
  active: true,
3506
3538
  streamCount: alive.length,
@@ -3509,8 +3541,43 @@ class ChatClient extends EventEmitter {
3509
3541
  phase: slot.phase,
3510
3542
  label: slot.phase ? (this._assistantPhaseLabels[slot.phase] || null) : null,
3511
3543
  text: slot.textAvailable ? (slot.text || null) : null,
3512
- textAvailable: slot.textAvailable
3513
- });
3544
+ textAvailable: slot.textAvailable,
3545
+ // stream 단위 UI(권장 (B))가 마운트 즉시 버블을 복원할 수 있도록 전부 싣는다.
3546
+ // 도착 순서(order)대로 — 소비자가 "가장 최근"을 다시 계산하지 않아도 되게 한다.
3547
+ streams: alive
3548
+ .sort((a, b) => a[1].order - b[1].order)
3549
+ .map(([id, s]) => ({
3550
+ streamId: id,
3551
+ personaId: s.personaId,
3552
+ phase: s.phase,
3553
+ label: s.phase ? (this._assistantPhaseLabels[s.phase] || null) : null,
3554
+ text: s.textAvailable ? (s.text || null) : null,
3555
+ textAvailable: s.textAvailable,
3556
+ resumed: s.resumed === true
3557
+ }))
3558
+ };
3559
+ }
3560
+
3561
+ /**
3562
+ * 방의 <b>현재</b> AI 진행 상태를 즉시 반환 (네트워크 호출 없음).
3563
+ *
3564
+ * <p>{@code assistantProgress} / {@code assistantRoomProgress} 는 transient 이벤트라, 방 화면을 나갔다
3565
+ * 들어오면 프론트 상태가 비어 있는데 <b>이미 지나간 이벤트는 다시 오지 않는다</b>. 마운트 시점에 이걸
3566
+ * 한 번 읽으면 다음 이벤트를 기다리지 않고 바로 그릴 수 있다 — 재입장 스냅샷(1 RTT)보다도 빠르다.</p>
3567
+ *
3568
+ * <p>반환 형태는 {@code assistantRoomProgress} 이벤트와 동일하고, stream 단위 UI 를 위해 {@code streams}
3569
+ * 배열이 추가된다. 진행 중인 것이 없으면 {@code active:false} + 빈 배열.</p>
3570
+ *
3571
+ * @param {string} roomId - 채팅방 ID
3572
+ * @returns {Object} { roomId, active, streamCount, streamId, personaId, phase, label, text, textAvailable, streams[] }
3573
+ *
3574
+ * @example
3575
+ * // 방 화면 마운트 시 — 이벤트를 기다리지 않고 즉시 복원
3576
+ * const p = client.chat.getAssistantProgress(roomId);
3577
+ * if (p.active) renderBubble(p);
3578
+ */
3579
+ getAssistantProgress(roomId) {
3580
+ return this._assistantRoomProgressPayload(roomId);
3514
3581
  }
3515
3582
 
3516
3583
  /**
package/src/constants.js CHANGED
@@ -364,7 +364,7 @@ export const WebSocketPaths = {
364
364
  getChatTypingDestination: (roomId) => `/topic/chat/${roomId}/typing`,
365
365
  // AI 진행 표시(생각중/검색중/작성중) + 토큰 스트리밍 — typing 과 별개 채널(가산 UX 레이어).
366
366
  getChatAssistantStreamDestination: (roomId) => `/topic/chat/${roomId}/assistant-stream`,
367
- // Rolling 누적 요약 완료 알림(READY/NO_CHANGE/FAILED) — refreshSummary 의 비동기 완료 통지 채널.
367
+ // Rolling 누적 요약 종결 알림(READY/NO_CHANGE/NO_MESSAGES/FAILED) — refreshSummary 비동기 완료 채널.
368
368
  getChatSummaryDestination: (roomId) => `/topic/chat/${roomId}/summary`,
369
369
 
370
370
  // Room list (카톡 스타일 리스트 실시간 업데이트)
@@ -52,6 +52,10 @@ const CHAT_DELEGATE_METHODS = [
52
52
  'isSubscribed',
53
53
  'startTyping',
54
54
  'stopTyping',
55
+ // AI 진행 표시 — 마운트 즉시 복원용 동기 조회 + 수동 재개. 여기 없으면 최상위 클라이언트에서
56
+ // 런타임 undefined 가 된다(타입 선언만 있고 배선이 없으면 타입이 거짓말을 한다).
57
+ 'getAssistantProgress',
58
+ 'resumeAssistantStreams',
55
59
  'getAssistants',
56
60
  'getRoomAiMeta',
57
61
  'rateAssistantMessage',
@@ -13,6 +13,7 @@ import type {
13
13
  ChatMessage,
14
14
  ChatMessageTypeValue,
15
15
  FileMetaData,
16
+ RoomSummaryView,
16
17
  SendFileInfo
17
18
  } from '../index';
18
19
  import { ChatMessageType } from '../index';
@@ -65,4 +66,24 @@ function legacyResponseFile(f: FileMetaData): string | undefined {
65
66
  return f.fileId;
66
67
  }
67
68
 
68
- export { category, viaTopLevel, mixed, gallery, document, render, uploadThenSend, optimistic, legacyResponseFile };
69
+ // 7) 서버 rolling 배포를 견디도록 summary 상태는 열린 문자열 union이어야 한다.
70
+ function renderSummaryStatus(status: NonNullable<RoomSummaryView['roomStatus']>): string {
71
+ switch (status) {
72
+ case 'QUEUED': return 'queued';
73
+ case 'IDLE': return 'idle';
74
+ case 'RUNNING': return 'running';
75
+ case 'FAILED': return 'failed';
76
+ case 'NO_MESSAGES': return 'empty';
77
+ default: {
78
+ // @ts-expect-error 열린 union이므로 알려진 값만 처리해도 exhaustive never가 될 수 없다.
79
+ const futureServerStatus: never = status;
80
+ void futureServerStatus;
81
+ return 'unknown';
82
+ }
83
+ }
84
+ }
85
+
86
+ const futureSummaryStatus: NonNullable<RoomSummaryView['roomStatus']> = 'FUTURE_SERVER_STATUS';
87
+
88
+ export { category, viaTopLevel, mixed, gallery, document, render, uploadThenSend, optimistic, legacyResponseFile,
89
+ renderSummaryStatus, futureSummaryStatus };
package/types/index.d.ts CHANGED
@@ -327,16 +327,20 @@ export interface RefreshSummaryOptions {
327
327
  export interface RefreshSummaryResult {
328
328
  /**
329
329
  * <ul>
330
- * <li>{@code 'STARTED'} — 요약 워커 시작됨({@code summaryUpdate} 대기).</li>
331
- * <li>{@code 'ALREADY_RUNNING'} — 방에서 요약이 진행 중(<b>다른</b> 포맷·언어일 수 있음). {@code format}/{@code language}
332
- * 로 내 키인지 확인하고, {@code summaryUpdate} 가 내 키가 아니면 그때 다시 요청한다(무조건 재요청 금지 아님).</li>
333
- * <li>{@code 'NO_CHANGE'} — 새 대화 없음(알림 발생 안 함; 직전 실패가 있었다면 자동 복구됨).</li>
330
+ * <li>{@code 'QUEUED'} — 방 단위 durable work item 에 저장·병합됐다. pod 실행 슬롯이 포화여도
331
+ * 요청은 버려지지 않으며, 최종 {@code READY|NO_CHANGE|NO_MESSAGES|FAILED} {@code summaryUpdate} 를 기다린다.</li>
334
332
  * </ul>
333
+ *
334
+ * <p>durable 저장 자체가 실패하면 성공 결과가 아니라 <b>HTTP 503</b>
335
+ * ({@code SUMMARY_REFRESH_UNAVAILABLE})으로 오며, {@link ChatClient.refreshSummary} 가 reject 한다.</p>
336
+ *
337
+ * <p>⚠️ <b>닫힌 union 이 아니다</b>: 서버가 값을 추가할 수 있으므로 분기에는 반드시 default 를 둔다.
338
+ * 모르는 값을 {@code QUEUED} 로 가정하지 말고 별도 상태 확인 UI를 표시한다.</p>
335
339
  */
336
- status: 'STARTED' | 'ALREADY_RUNNING' | 'NO_CHANGE';
337
- /** 이번 요청이 해소한 포맷({@link SummarizeFormat} 소문자). 방 단위 경합 식별용. */
340
+ status: 'QUEUED' | (string & {});
341
+ /** durable work item 에 병합한 포맷({@link SummarizeFormat} 소문자). */
338
342
  format?: string;
339
- /** 이번 요청이 해소한 언어(소문자 BCP-47 / {@code 'OFF'}=원문). 방 단위 경합 식별용. */
343
+ /** durable work item 에 병합한 요청자 언어(소문자 BCP-47 / {@code 'OFF'}=원문). */
340
344
  language?: string;
341
345
  }
342
346
 
@@ -400,11 +404,15 @@ export interface RoomSummaryView {
400
404
  */
401
405
  language: string;
402
406
  /**
403
- * 방 전체 요약 프로세스 상태 — {@code 'IDLE'}(대기) | {@code 'RUNNING'}(요약 중) | {@code 'FAILED'}(실패).
404
- * <b>방 단위(키별 아님)</b>: 방은 한 번에 한 키만 요약하므로, 지금 다른 포맷·언어를 요약 중이어도 {@code 'RUNNING'} 이다.
405
- * 특정 rollup 의 최신 여부는 {@code rollup.stale} 로 판단한다(rollup 자체는 완료된 데이터). 요약 이력이 없으면 null.
407
+ * 실행 상태 — {@code 'QUEUED'}(durable 대기) | {@code 'IDLE'}(유휴) |
408
+ * {@code 'RUNNING'}(요약 중) | {@code 'FAILED'}(선택 키 실패) |
409
+ * {@code 'NO_MESSAGES'}(선택 키에 요약할 대화 없음).
410
+ * 대기/실행은 방 단위이고 terminal 결과는 조회한 포맷×언어 키 기준이다. 특정 rollup 의 최신 여부는
411
+ * {@code rollup.stale} 로 판단한다. 요약 이력이 없어도 durable job 상태가 있으면 null이 아닐 수 있다.
412
+ * 닫힌 union이 아니므로 분기에는 default를 둔다.
406
413
  */
407
- roomStatus: 'IDLE' | 'RUNNING' | 'FAILED' | null;
414
+ /** durable execution-plane 상태에서 합성되므로 재접속 뒤에도 복원된다. */
415
+ roomStatus: 'QUEUED' | 'IDLE' | 'RUNNING' | 'FAILED' | 'NO_MESSAGES' | (string & {}) | null;
408
416
  /** 요청/기본 포맷의 통합 요약본. 그 포맷×언어로 아직 요약 안 됐으면 null(availableFormats 로 같은 언어의 다른 포맷 확인). */
409
417
  rollup: RoomSummaryRollup | null;
410
418
  /** 이 방이 <b>이 언어로</b> 보유한 포맷 목록(short/timeline/… — 탭 렌더용). 타 언어는 포함하지 않는다. */
@@ -424,15 +432,18 @@ export interface RoomSummaryView {
424
432
  export interface SummaryUpdateEvent {
425
433
  roomId: string;
426
434
  /**
427
- * {@code 'READY'}(갱신됨 → getSummary) | {@code 'FAILED'}(실패 → 재시도 가능) | {@code 'NO_CHANGE'}(변경 없음) |
435
+ * {@code 'READY'}(갱신됨 → getSummary) | {@code 'FAILED'}(실패 → 재시도 가능) |
436
+ * {@code 'NO_CHANGE'}(변경 없이 durable 작업 종결 → getSummary로 최종 동기화) |
437
+ * {@code 'NO_MESSAGES'}(선택 키에 요약할 대화 없음) |
428
438
  * {@code 'INVALIDATED'}(방 원문 삭제/편집으로 요약이 통째로 버려짐 → 화면을 비우고 필요 시 refreshSummary 로 재생성).
439
+ * 닫힌 union이 아니므로 모르는 값은 추측하지 말고 one-shot getSummary로 재동기화한다.
429
440
  */
430
- status: 'READY' | 'NO_CHANGE' | 'FAILED' | 'INVALIDATED';
431
- /** 완료/실패한 포맷. {@code READY}·{@code FAILED} 포함, {@code NO_CHANGE}·{@code INVALIDATED} 는 null. */
441
+ status: 'READY' | 'NO_CHANGE' | 'NO_MESSAGES' | 'FAILED' | 'INVALIDATED' | (string & {});
442
+ /** 완료 포맷. {@code READY}·{@code NO_CHANGE}·{@code NO_MESSAGES}·{@code FAILED} 포함, {@code INVALIDATED} 는 null. */
432
443
  format: string | null;
433
444
  /**
434
- * fan-out 으로 한 번에 생성/최신화된 언어 목록(소문자 BCP-47 / {@code 'OFF'}). {@code READY}·{@code FAILED} 포함,
435
- * 그 외 null. 각 사용자는 자기 언어가 이 목록에 있으면 {@code getSummary({ format, lang })} 로 읽는다(§S1).
445
+ * fan-out 으로 한 번에 처리한 언어 목록(소문자 BCP-47 / {@code 'OFF'}).
446
+ * {@code READY}·{@code NO_CHANGE}·{@code NO_MESSAGES}·{@code FAILED} 포함, {@code INVALIDATED} 만 null.
436
447
  */
437
448
  languages: string[] | null;
438
449
  /** 갱신 시각(epoch millis). {@code READY} 만 포함, 그 외 null. */
@@ -1260,6 +1271,29 @@ export interface AssistantRoomProgressEvent {
1260
1271
  textAvailable?: boolean;
1261
1272
  }
1262
1273
 
1274
+ /** {@link ChatClient.getAssistantProgress} 가 돌려주는 stream 1건. */
1275
+ export interface AssistantProgressStreamSnapshot {
1276
+ streamId: string;
1277
+ personaId: string | null;
1278
+ phase: 'THINKING' | 'SEARCHING' | 'PLANNING' | 'WRITING' | null;
1279
+ label: string | null;
1280
+ text: string | null;
1281
+ textAvailable: boolean;
1282
+ /** 서버 스냅샷으로 복원된 진행분이면 true. */
1283
+ resumed: boolean;
1284
+ }
1285
+
1286
+ /**
1287
+ * 방의 **현재** AI 진행 상태 (v5.3.0 — {@link ChatClient.getAssistantProgress} 반환).
1288
+ *
1289
+ * <p>{@link AssistantRoomProgressEvent} 와 같은 형태에 stream 단위 UI 를 위한 `streams` 가 더해진다.
1290
+ * 이벤트는 transient 라 화면을 나갔다 들어오면 다시 오지 않는다 — 마운트 시 이걸 한 번 읽는다.</p>
1291
+ */
1292
+ export interface AssistantProgressSnapshot extends AssistantRoomProgressEvent {
1293
+ /** 살아 있는 stream 전체 (도착 순서). 없으면 빈 배열. */
1294
+ streams: AssistantProgressStreamSnapshot[];
1295
+ }
1296
+
1263
1297
  // ============================================================================
1264
1298
  // Options — constructor & method
1265
1299
  // ============================================================================
@@ -2017,6 +2051,17 @@ export class ChatClient implements SdkEventEmitter<ChatClientEvents> {
2017
2051
 
2018
2052
  startTyping(roomId: string): void;
2019
2053
  stopTyping(roomId: string): void;
2054
+
2055
+ /**
2056
+ * 방의 현재 AI 진행 상태를 즉시 반환 (네트워크 호출 없음, v5.3.0).
2057
+ * 진행 표시 이벤트는 transient 라 방 화면 마운트 시 이걸 읽어 버블을 복원한다.
2058
+ */
2059
+ getAssistantProgress(roomId: string): AssistantProgressSnapshot;
2060
+ /**
2061
+ * 구독 중인 모든 방의 진행 스트림 스냅샷을 서버에서 다시 받는다 (v5.1.0).
2062
+ * 재연결·탭 복귀 때 SDK 가 자동 호출하므로 보통은 직접 부를 일이 없다.
2063
+ */
2064
+ resumeAssistantStreams(reason?: string): Promise<void>;
2020
2065
  }
2021
2066
 
2022
2067
  export class WebRTCClient implements SdkEventEmitter<WebRTCClientEvents> {
@@ -2400,6 +2445,17 @@ export default class TalkFlowClient implements SdkEventEmitter<TalkFlowClientEve
2400
2445
  startTyping(roomId: string): void;
2401
2446
  stopTyping(roomId: string): void;
2402
2447
 
2448
+ /**
2449
+ * 방의 현재 AI 진행 상태를 즉시 반환 (네트워크 호출 없음, v5.3.0).
2450
+ * 진행 표시 이벤트는 transient 라 방 화면 마운트 시 이걸 읽어 버블을 복원한다.
2451
+ */
2452
+ getAssistantProgress(roomId: string): AssistantProgressSnapshot;
2453
+ /**
2454
+ * 구독 중인 모든 방의 진행 스트림 스냅샷을 서버에서 다시 받는다 (v5.1.0).
2455
+ * 재연결·탭 복귀 때 SDK 가 자동 호출하므로 보통은 직접 부를 일이 없다.
2456
+ */
2457
+ resumeAssistantStreams(reason?: string): Promise<void>;
2458
+
2403
2459
  // WebRTC delegations
2404
2460
  initializeIceServers(): Promise<RTCIceServer[]>;
2405
2461
  getTurnCredentials(): Promise<{ iceServers: RTCIceServer[] }>;