@turns/dev 0.0.20 → 0.0.22

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.
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
3
4
  import fs from 'node:fs';
4
5
  import net from 'node:net';
5
6
  import os from 'node:os';
@@ -598,7 +599,12 @@ class CodexAppServerTransport extends LineTransport {
598
599
  asyncQuestionsSeen = new Set();
599
600
  constructor(anchor) {
600
601
  const profile = { command: 'codex' };
601
- const mcp = `mcp_servers.turns={command=${JSON.stringify(process.execPath)},args=[${JSON.stringify(questionMcpPath)}],env={TURNS_AGENT_ANCHOR_SOCKET=${JSON.stringify(socketPath)}}}`;
602
+ // Codex deep-merges CLI overrides with configured MCP servers. Reusing
603
+ // "turns" mixes our stdio command with a user's HTTP URL and prevents even
604
+ // thread/start from loading its config. Keep the helper stable per anchor,
605
+ // but outside user-configured names, without rewriting their config.
606
+ const mcpName = `turns_anchor_${createHash('sha256').update(socketPath).digest('hex').slice(0, 12)}`;
607
+ const mcp = `mcp_servers.${mcpName}={command=${JSON.stringify(process.execPath)},args=[${JSON.stringify(questionMcpPath)}],env={TURNS_AGENT_ANCHOR_SOCKET=${JSON.stringify(socketPath)}}}`;
602
608
  super(anchor, 'app-server', resolveHarnessCommand('codex', profile), ['-c', mcp, 'app-server', '--listen', 'stdio://'], anchor.cwd);
603
609
  this.threadId = anchor.agentSessionId;
604
610
  this.permissionMode = anchor.permissionMode;
@@ -24,6 +24,11 @@ const MAX_BATCH_ROWS = 100;
24
24
  const MAX_RUNTIME_EVENT_BATCH = 250;
25
25
  const RPC_TIMEOUT_MS = 5 * 60_000;
26
26
  const MAX_SOCKET_RETRIES = 8;
27
+ /** Deadline for an uploader socket to finish its TCP, TLS, and upgrade
28
+ * handshake and receive the server's `ready` acknowledgement. A stalled
29
+ * handshake emits neither `error` nor `close`, so without this the daemon
30
+ * would wait in CONNECTING forever and silently stop uploading. */
31
+ const READY_TIMEOUT_MS = 30_000;
27
32
  const MAX_NATIVE_SETTLE_PASSES = 4;
28
33
  const MAX_EXTERNAL_STATUS_BATCH = 1_000;
29
34
  const NATIVE_STATE_VERSION = 1;
@@ -548,28 +553,43 @@ export class RpcSocket {
548
553
  resolveClosed;
549
554
  ready;
550
555
  socket;
551
- constructor(url, token, devUser = '') {
556
+ constructor(url, token, devUser = '', { readyTimeoutMs = READY_TIMEOUT_MS } = {}) {
552
557
  this.pending = new Map();
553
558
  this.listeners = new Set();
554
559
  this.closed = new Promise((resolve) => {
555
560
  this.resolveClosed = resolve;
556
561
  });
557
562
  this.ready = new Promise((resolve, reject) => {
563
+ // Settle exactly once so the deadline, the socket's own events, and the
564
+ // abort error that `terminate()` raises afterwards cannot race.
565
+ let settled = false;
566
+ let readyTimer = null;
567
+ const settle = (complete, value) => {
568
+ if (settled)
569
+ return;
570
+ settled = true;
571
+ clearTimeout(readyTimer);
572
+ complete(value);
573
+ };
574
+ readyTimer = setTimeout(() => {
575
+ settle(reject, socketError(`Turns did not acknowledge the uploader within ${readyTimeoutMs / 1000} seconds.`));
576
+ this.socket.terminate();
577
+ }, readyTimeoutMs);
558
578
  const headers = devUser
559
579
  ? { 'x-turns-dev-user': devUser, 'x-turns-dev-role': 'uploader' }
560
580
  : { Authorization: `Bearer ${token}` };
561
- this.socket = new WebSocket(url, { headers });
581
+ this.socket = new WebSocket(url, { headers, handshakeTimeout: readyTimeoutMs });
562
582
  this.socket.once('unexpected-response', (_request, response) => {
563
583
  response.resume?.();
564
584
  if (response.statusCode === 401 || response.statusCode === 403) {
565
- reject(authenticationError(`Turns rejected uploader authentication (HTTP ${response.statusCode}).`));
585
+ settle(reject, authenticationError(`Turns rejected uploader authentication (HTTP ${response.statusCode}).`));
566
586
  }
567
587
  else {
568
- reject(socketError(`Unexpected WebSocket response (HTTP ${response.statusCode}).`));
588
+ settle(reject, socketError(`Unexpected WebSocket response (HTTP ${response.statusCode}).`));
569
589
  }
570
590
  });
571
- this.socket.once('error', (error) => reject(socketError(`WebSocket connection failed: ${error.message}`, error)));
572
- this.socket.on('message', (data) => this.onMessage(String(data), resolve, reject));
591
+ this.socket.once('error', (error) => settle(reject, socketError(`WebSocket connection failed: ${error.message}`, error)));
592
+ this.socket.on('message', (data) => this.onMessage(String(data), (ready) => settle(resolve, ready), (error) => settle(reject, error)));
573
593
  this.socket.on('close', (code, reason) => {
574
594
  const error = socketError(`WebSocket closed (${code}): ${String(reason)}`);
575
595
  for (const request of this.pending.values()) {
@@ -665,7 +685,9 @@ export async function connectUploader(options, machine, reconnect = false) {
665
685
  const credentials = await options.credentialProvider.refreshIfDue();
666
686
  options.token = credentials.accessToken;
667
687
  }
668
- const rpc = new RpcSocket(options.url, options.token, options.devUser);
688
+ const rpc = new RpcSocket(options.url, options.token, options.devUser, {
689
+ readyTimeoutMs: options.readyTimeoutMs,
690
+ });
669
691
  try {
670
692
  const ready = await rpc.ready;
671
693
  process.stdout.write(`${reconnect ? 'Reconnected' : 'Connected'} to ${options.url} as @${ready.user.login} ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turns/dev",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "description": "Upload local AI agent sessions to turns.dev",
5
5
  "license": "MIT",
6
6
  "type": "module",