@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.
- package/dist/agent-anchor.js +7 -1
- package/dist/upload-sessions.js +29 -7
- package/package.json +1 -1
package/dist/agent-anchor.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/upload-sessions.js
CHANGED
|
@@ -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
|
|
585
|
+
settle(reject, authenticationError(`Turns rejected uploader authentication (HTTP ${response.statusCode}).`));
|
|
566
586
|
}
|
|
567
587
|
else {
|
|
568
|
-
reject
|
|
588
|
+
settle(reject, socketError(`Unexpected WebSocket response (HTTP ${response.statusCode}).`));
|
|
569
589
|
}
|
|
570
590
|
});
|
|
571
|
-
this.socket.once('error', (error) => reject
|
|
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} ` +
|