osborn 0.9.74 → 0.9.76
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/dist/index.js +35 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ initializeLogger({ pretty: true, level: 'info' });
|
|
|
10
10
|
import { setMaxListeners } from 'node:events';
|
|
11
11
|
setMaxListeners(50);
|
|
12
12
|
import { createServer } from 'http';
|
|
13
|
-
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, mkdtempSync, cpSync, rmSync, statSync, createWriteStream } from 'node:fs';
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, mkdtempSync, cpSync, rmSync, statSync, utimesSync, createWriteStream } from 'node:fs';
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
@@ -603,6 +603,18 @@ function startApiServer(workingDir, port) {
|
|
|
603
603
|
// the only structural change; file contents are immutable historical
|
|
604
604
|
// record and must roundtrip cleanly between environments.
|
|
605
605
|
cpSync(sp, dp, { force: true });
|
|
606
|
+
// 0.9.75: preserve the source mtime on the copy. cpSync stamps the
|
|
607
|
+
// destination with "now", which scrambled session ordering after
|
|
608
|
+
// every sync — the frontend's auto-resume picks most-recent-by-mtime,
|
|
609
|
+
// so a fresh import made a random tar-order file (often a tiny
|
|
610
|
+
// months-old session) look newest, and users kept "resuming" into
|
|
611
|
+
// stale June sessions (confirmed in prod 2026-07-27, 3× in a row).
|
|
612
|
+
// The tarball carries original mtimes; copying must not discard them.
|
|
613
|
+
try {
|
|
614
|
+
const srcStat = statSync(sp);
|
|
615
|
+
utimesSync(dp, srcStat.atime, srcStat.mtime);
|
|
616
|
+
}
|
|
617
|
+
catch { /* best-effort — ordering degrades gracefully */ }
|
|
606
618
|
filesWritten++;
|
|
607
619
|
}
|
|
608
620
|
// skip symlinks, sockets, etc.
|
|
@@ -1302,7 +1314,18 @@ async function main() {
|
|
|
1302
1314
|
console.log(`🧟 [ZOMBIE-WATCHDOG] room empty for ${emptyPolls} consecutive polls (${emptyForMs / 1000}s) — forcing leave`);
|
|
1303
1315
|
emptyPolls = 0;
|
|
1304
1316
|
intentionalLeave = true;
|
|
1305
|
-
|
|
1317
|
+
// Update state FIRST, don't wait for the Disconnected event — a zombie
|
|
1318
|
+
// WS is precisely the case where that event never arrives. Observed
|
|
1319
|
+
// 2026-07-27: after a forced leave the status stayed 'connected', so
|
|
1320
|
+
// /health lied, /connect-room no-op'd, and the next user joined a room
|
|
1321
|
+
// the agent wasn't in (UI stuck at "Connecting..."). Setting idle here
|
|
1322
|
+
// makes the next /connect-room actually rejoin. If the Disconnected
|
|
1323
|
+
// event does fire later, its handler sets the same state — idempotent.
|
|
1324
|
+
livekitState.status = 'idle';
|
|
1325
|
+
const disconnectWatch = setTimeout(() => console.error('🧟 [ZOMBIE-WATCHDOG] room.disconnect() still pending after 10s — WS likely zombied; idle-exit timer is the backstop'), 10_000);
|
|
1326
|
+
room.disconnect()
|
|
1327
|
+
.then(() => { clearTimeout(disconnectWatch); console.log('🧟 [ZOMBIE-WATCHDOG] forced leave completed'); })
|
|
1328
|
+
.catch((e) => { clearTimeout(disconnectWatch); console.error('watchdog room.disconnect failed:', e); });
|
|
1306
1329
|
// Belt-and-suspenders: if the WS is zombied the Disconnected event may
|
|
1307
1330
|
// never fire (so the idle-exit arm in that handler won't run). Arm here too.
|
|
1308
1331
|
armIdleExitTimer('zombie-watchdog forced leave');
|
|
@@ -4724,6 +4747,16 @@ async function main() {
|
|
|
4724
4747
|
return; // already in the room — no-op
|
|
4725
4748
|
console.log('🔌 /connect-room — joining LiveKit for incoming user');
|
|
4726
4749
|
await connectWithRetry();
|
|
4750
|
+
// Join-race fix: ParticipantConnected only fires for participants who join
|
|
4751
|
+
// AFTER us. If the user's browser won the race (its token mint + WebRTC
|
|
4752
|
+
// handshake finished before our rejoin), they're already in the room when
|
|
4753
|
+
// we arrive — no event, no voice session, UI hangs at "Connecting...".
|
|
4754
|
+
// Re-emit the event for every pre-existing participant so the session is
|
|
4755
|
+
// created exactly as if they had joined after us.
|
|
4756
|
+
for (const p of room.remoteParticipants.values()) {
|
|
4757
|
+
console.log(`👥 Participant already in room at join: ${p.identity} — adopting (re-emitting ParticipantConnected)`);
|
|
4758
|
+
room.emit(RoomEvent.ParticipantConnected, p);
|
|
4759
|
+
}
|
|
4727
4760
|
};
|
|
4728
4761
|
// 0.9.73: explicit frontend leave should also start the idle-exit countdown —
|
|
4729
4762
|
// the user is done; the machine shouldn't idle-bill waiting for a maybe-return.
|