@dmsdc-ai/aigentry-telepty 0.5.2 → 0.5.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.
package/cli.js CHANGED
@@ -1273,6 +1273,10 @@ async function main() {
1273
1273
  let wsReady = false;
1274
1274
  let reconnectAttempts = 0;
1275
1275
  let reconnectTimer = null;
1276
+ // BUG-C: the daemon mints a per-owner token on each owner claim/reclaim and pushes it here.
1277
+ // We echo it on the teardown DELETE so the daemon can tell our (current-owner) exit apart
1278
+ // from a stale/displaced owner's exit and avoid the shared-fate teardown.
1279
+ let currentOwnerToken = null;
1276
1280
  let lastInjectTextTime = 0;
1277
1281
  const MAX_RECONNECT_DELAY = 30000;
1278
1282
 
@@ -1318,6 +1322,10 @@ async function main() {
1318
1322
  daemonWs.on('message', (message) => {
1319
1323
  try {
1320
1324
  const msg = JSON.parse(message);
1325
+ if (msg.type === 'owner_token') {
1326
+ currentOwnerToken = msg.token || null;
1327
+ return;
1328
+ }
1321
1329
  if (msg.type === 'inject') {
1322
1330
  const chunks = [];
1323
1331
  const rawData = typeof msg.data === 'string' ? msg.data : String(msg.data ?? '');
@@ -1430,7 +1438,12 @@ async function main() {
1430
1438
  // Purge bridge mailbox on clean exit (undelivered messages are stale)
1431
1439
  try { bridgeMailbox.purge(bridgeTarget); } catch {}
1432
1440
  process.stdout.write(`\x1b]0;\x07`);
1433
- fetchWithAuth(`${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}`, { method: 'DELETE' }).catch(() => {});
1441
+ // BUG-C: carry our owner token so the daemon destroys only on the CURRENT owner's exit;
1442
+ // a stale/displaced owner's DELETE (mismatched token) must not tear down the live owner.
1443
+ const deleteUrl = currentOwnerToken
1444
+ ? `${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}?owner_token=${encodeURIComponent(currentOwnerToken)}`
1445
+ : `${DAEMON_URL}/api/sessions/${encodeURIComponent(sessionId)}`;
1446
+ fetchWithAuth(deleteUrl, { method: 'DELETE' }).catch(() => {});
1434
1447
  if (reconnectTimer) clearTimeout(reconnectTimer);
1435
1448
  try {
1436
1449
  daemonWs.close();
package/daemon.js CHANGED
@@ -2757,6 +2757,18 @@ app.delete('/api/sessions/:id', (req, res) => {
2757
2757
  const session = sessions[resolvedId];
2758
2758
  const id = resolvedId;
2759
2759
  if (session.isClosing) return res.json({ success: true, status: 'closing' });
2760
+ // BUG-C (shared-fate): a wrapped session can be co-bound by a stale/displaced owner bridge
2761
+ // (duplicate --id). A DELETE carrying a token that is NOT the current owner's, while a live
2762
+ // owner ws is still open, is that stale bridge exiting — it must NOT tear down the live owner.
2763
+ // Detach-only (no-op): leave the record and every client untouched. Tokenless callers
2764
+ // (operator `telepty delete`, ghost clean) and matching-token current-owner exits are
2765
+ // unaffected. Forceful kills go through POST /:id/kill (teardownSessionById), not here.
2766
+ const ownerToken = req.query.owner_token;
2767
+ if (session.type === 'wrapped'
2768
+ && ownerToken && session.ownerToken && ownerToken !== session.ownerToken
2769
+ && isOpenWebSocket(session.ownerWs)) {
2770
+ return res.json({ success: true, status: 'stale-detached' });
2771
+ }
2760
2772
  try {
2761
2773
  session.isClosing = true;
2762
2774
  if (session.type === 'wrapped') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "main": "daemon.js",
5
5
  "bin": {
6
6
  "aigentry-telepty": "install.js",
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ const crypto = require('node:crypto');
3
4
  const { WebSocketServer } = require('ws');
4
5
 
5
6
  function isOpenWebSocket(ws) {
@@ -107,6 +108,13 @@ function installWebSocketTransport(deps) {
107
108
  activeSession.ownerWs.terminate();
108
109
  }
109
110
  activeSession.ownerWs = ws;
111
+ // BUG-C: mint a fresh per-owner token on every claim/reclaim and push it to this owner.
112
+ // The token is the exact "are-you-the-current-owner" discriminator the DELETE guard uses
113
+ // to suppress a stale/displaced owner's teardown (shared-fate fix). Reclaim refreshes it,
114
+ // so the live current owner always holds the current token while a displaced owner keeps a
115
+ // stale one.
116
+ activeSession.ownerToken = crypto.randomUUID();
117
+ try { ws.send(JSON.stringify({ type: 'owner_token', token: activeSession.ownerToken })); } catch {}
110
118
  markSessionConnected(activeSession);
111
119
  initializeBootstrapState(activeSession);
112
120
  console.log(`[WS] Wrap owner ${isOwnerConnect && activeSession.clients.size > 1 ? 're-' : ''}connected for session ${sessionId} (Total: ${activeSession.clients.size})`);