shellbase 0.13.2 → 0.13.3

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 (2) hide show
  1. package/dist/agent.js +50 -13
  2. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -57,6 +57,12 @@ const MAX_PENDING_OUTPUT = 256 * 1024;
57
57
  // 서버가 40회 연속 전송도 거부 없이 받아줬고 이 값이 타이핑 체감 지연의 주범이었음(입력·출력 양쪽에서
58
58
  // 각각 최대 120ms). 30ms 로 낮춰 체감을 줄이고, 출력이 몰릴 때는 여전히 초당 33회로 묶인다.
59
59
  const MIN_SEND_INTERVAL_MS = 30;
60
+ // 서버 답(ack)을 안 기다리고 잇달아 부칠 수 있는 조각 수. 답을 하나씩 기다리면 조각마다 왕복
61
+ // 한 번(실측 276ms)이 통째로 지연이 되어, 출력이 조금만 나뉘어도 몇 초 뒤에 한꺼번에 나타났다
62
+ // (10조각 = 2.7초. 같은 것을 잇달아 부치면 0.29초 — 순서도 그대로 지켜지는 것을 실측 확인).
63
+ // 그래도 무한정 부치면 통로가 막혔을 때 밀린 조각이 계속 쌓이므로, 이만큼만 부쳐두고
64
+ // 답이 하나 돌아올 때마다 이어서 보낸다 (막히면 저절로 모아 보내는 효과도 있다).
65
+ const MAX_FRAMES_IN_FLIGHT = 6;
60
66
  // 그래도 서버가 거부하면(RATE_LIMITED 등) 잠시 간격을 넉넉히 벌려서 재시도한다
61
67
  const SEND_PENALTY_MS = 500;
62
68
  const PENALTY_INTERVAL_MS = 250;
@@ -325,23 +331,49 @@ export async function runAgent(options) {
325
331
  }
326
332
  session.flushing = true;
327
333
  session.lastSentAt = Date.now();
334
+ // 부치는 *순서*만 이 줄에서 지키고, **서버 답(ack)은 기다리지 않는다**.
335
+ // 예전에는 조각 하나를 부치고 답이 올 때까지(왕복 276ms) 다음 조각을 붙들고 있었다.
336
+ // 그래서 출력이 열 조각으로 나뉘면 2.7초 뒤에야 마지막이 도착했고, 화면에는 한참 아무것도
337
+ // 없다가 한꺼번에 쏟아지는 것처럼 보였다 — "타자가 2초 뒤에 몰아서 보인다" 의 정체.
328
338
  await enqueueSend(session, async () => {
329
339
  try {
330
340
  // 프레임 한도를 넘지 않도록 나눠 보낸다 (한 덩이로 보내면 통째로 거부돼 화면이 멈춘다)
331
341
  const parts = chunkByBytes(pending, frameBudget);
332
- const sent = await sendParts(session, parts);
333
- if (sent < parts.length) {
334
- if (parts.length === 1) {
335
- // 조각이 하나뿐인데 못 갔다 = 아무것도 갔다. 그대로 다음 전송 앞에 다시 붙인다.
336
- session.outBuffer = pending + session.outBuffer;
337
- }
338
- else if (!session.closing) {
339
- // 여러 조각을 한꺼번에 부쳤는데 일부만 갔다. 못 간 것만 다시 보내면 이미 도착한 뒷조각이
340
- // 두 번 그려질 수 있다 — 대신 화면을 통째로 다시 보낸다. 지금 화면이 어떻게 생겼는지는
341
- // 이쪽(session.screen) 정확히 들고 있어서, 빠지지도 겹치지도 않는다.
342
+ session.inFlight += parts.length;
343
+ // send() 부르는 순간 이미 통로로 나간다. 부른 순서 = 나가는 순서라서(실측 확인)
344
+ // 답을 기다리지 않아도 조각이 뒤섞이지 않는다.
345
+ const results = parts.map((part) => sendFrame({
346
+ kind: 'output',
347
+ to: session.deviceId,
348
+ data: encryptFrame(part, session.frameKey),
349
+ }));
350
+ void Promise.allSettled(results)
351
+ .then((settled) => {
352
+ let sent = 0;
353
+ while (sent < settled.length && settled[sent].status === 'fulfilled')
354
+ sent += 1;
355
+ if (sent >= settled.length)
356
+ return;
357
+ const reason = settled[sent].reason;
358
+ const message = String(reason?.message ?? reason);
359
+ console.error(`출력 전송 실패 (${sent}/${parts.length} 조각만 나감):`, message);
360
+ if (/too large|TOO_LARGE|size|크기/i.test(message))
361
+ shrinkFrameBudget(message);
362
+ session.sendPenaltyUntil = Date.now() + SEND_PENALTY_MS;
363
+ // 못 간 것을 다시 보내는 대신 **화면을 통째로 다시 보낸다**.
364
+ // 답을 기다리지 않고 잇달아 부치기 때문에, 실패를 알게 된 시점에는 그 뒤의 출력이
365
+ // 이미 나가 있을 수 있다 — 거기에 옛 조각을 뒤늦게 끼워 넣으면 화면이 뒤섞인다.
366
+ // 지금 화면이 어떻게 생겼는지는 session.screen 이 정확히 들고 있어서,
367
+ // 통째로 다시 보내면 빠지지도 겹치지도 않는다.
368
+ if (!session.closing)
342
369
  void sendRestore(session);
343
- }
344
- }
370
+ })
371
+ .finally(() => {
372
+ session.inFlight -= parts.length;
373
+ // 답을 기다리느라 모아둔 것이 있으면 이어서 보낸다
374
+ if (session.outBuffer && !session.closing && !session.restoring)
375
+ scheduleFlush(session);
376
+ });
345
377
  }
346
378
  catch (err) {
347
379
  // 여기서 새어 나간 오류는 아무도 받아주지 않아 프로세스가 통째로 죽는다 — 반드시 잡는다
@@ -351,7 +383,7 @@ export async function runAgent(options) {
351
383
  // 무슨 일이 있어도 표시는 반드시 푼다 — 여기 걸린 채로 남으면 그 세션은 그 뒤로
352
384
  // 아무 화면도 못 보낸다 (검은 화면의 원인 중 하나)
353
385
  session.flushing = false;
354
- // 남은 것(재시도분·보내는 사이에 새로 나온 것)이 있으면 이어서 보낸다
386
+ // 보내는 사이에 새로 나온 것이 있으면 곧바로 이어서 보낸다
355
387
  if (session.outBuffer && !session.closing && !session.restoring)
356
388
  scheduleFlush(session);
357
389
  }
@@ -458,6 +490,10 @@ export async function runAgent(options) {
458
490
  const scheduleFlush = (session) => {
459
491
  if (session.pendingFlush !== null || session.flushing || session.restoring)
460
492
  return;
493
+ // 답을 못 받은 조각이 너무 많이 쌓였으면 잠시 모아둔다 — 하나가 돌아오면 그때 이어서 보낸다
494
+ // (그 사이 나온 출력은 한 덩이로 합쳐져서, 막힌 상황에서도 조각 수가 늘지 않는다)
495
+ if (session.inFlight >= MAX_FRAMES_IN_FLIGHT)
496
+ return;
461
497
  const interval = Date.now() < session.sendPenaltyUntil ? PENALTY_INTERVAL_MS : MIN_SEND_INTERVAL_MS;
462
498
  const wait = Math.max(0, interval - (Date.now() - session.lastSentAt));
463
499
  session.pendingFlush = setTimeout(() => void flush(session), wait);
@@ -578,6 +614,7 @@ export async function runAgent(options) {
578
614
  approvalInFlight: false,
579
615
  outBuffer: '',
580
616
  flushing: false,
617
+ inFlight: 0,
581
618
  sendChain: Promise.resolve(),
582
619
  restoring: false,
583
620
  restoreAgain: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shellbase",
3
- "version": "0.13.2",
3
+ "version": "0.13.3",
4
4
  "description": "내 컴퓨터 터미널(특히 Claude Code 세션)을 폰 브라우저로 실시간 접속하게 해주는 데스크톱 에이전트",
5
5
  "type": "module",
6
6
  "bin": {