@turns/dev 0.0.22 → 0.0.24
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/README.md +7 -0
- package/dist/agent-anchor.js +17 -3
- package/dist/harness-registry.js +1 -1
- package/dist/index.js +7 -6
- package/dist/logging.js +14 -0
- package/dist/path-resolution.js +2 -1
- package/dist/queue-worker.js +20 -14
- package/dist/upload-sessions.js +29 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,13 @@ On macOS, credentials and daemon state live under
|
|
|
69
69
|
LaunchAgent `~/Library/LaunchAgents/dev.turns.cli.plist`. Its output is written
|
|
70
70
|
to `~/Library/Application Support/turns/daemon.log`.
|
|
71
71
|
|
|
72
|
+
Daemon runtime logs (including reconnects, upload progress, and errors) prefix
|
|
73
|
+
each line with a UTC ISO 8601 timestamp, for example
|
|
74
|
+
`[2026-10-19T09:12:34.567Z] Reconnected to wss://turns.dev/api/socket ...`.
|
|
75
|
+
Foreground uploads use the same timestamped runtime logs. Multiline messages
|
|
76
|
+
share one timestamp and retain their indentation; stdout and stderr remain
|
|
77
|
+
separate. Help, version, and service-management output keep their plain format.
|
|
78
|
+
|
|
72
79
|
The credential file contains a seven-day access token and a signed refresh
|
|
73
80
|
token. The CLI refreshes the access token at least once every 24 hours. A new
|
|
74
81
|
machine opens a one-time turns.dev URL, waits on an authentication WebSocket,
|
package/dist/agent-anchor.js
CHANGED
|
@@ -209,6 +209,8 @@ class LineTransport {
|
|
|
209
209
|
*/
|
|
210
210
|
const CLAUDE_NOTIFICATION_RESULT_GRACE_MS = 2_000;
|
|
211
211
|
class ClaudeTransport extends LineTransport {
|
|
212
|
+
model;
|
|
213
|
+
thinkingLevel;
|
|
212
214
|
nativeTurnActive = false;
|
|
213
215
|
sessionId;
|
|
214
216
|
permissionMode;
|
|
@@ -242,6 +244,8 @@ class ClaudeTransport extends LineTransport {
|
|
|
242
244
|
super(anchor, 'claude', resolveHarnessCommand('claude', profile), args, anchor.cwd, {
|
|
243
245
|
CLAUDE_CODE_ENTRYPOINT: 'turns-agent-anchor',
|
|
244
246
|
});
|
|
247
|
+
this.model = anchor.model;
|
|
248
|
+
this.thinkingLevel = anchor.thinkingLevel;
|
|
245
249
|
this.sessionId = anchor.agentSessionId;
|
|
246
250
|
this.permissionMode = anchor.permissionMode;
|
|
247
251
|
this.turnWaiter = null;
|
|
@@ -789,7 +793,12 @@ class CodexAppServerTransport extends LineTransport {
|
|
|
789
793
|
this.turnId = null;
|
|
790
794
|
this.turnIdWaiter = deferred();
|
|
791
795
|
this.turnWaiter = waiter;
|
|
792
|
-
const started = await this.request('turn/start', {
|
|
796
|
+
const started = await this.request('turn/start', {
|
|
797
|
+
threadId: this.threadId,
|
|
798
|
+
input: [{ type: 'text', text }],
|
|
799
|
+
...(this.anchor.model && this.anchor.model !== 'auto' ? { model: this.anchor.model } : {}),
|
|
800
|
+
...(this.anchor.thinkingLevel ? { effort: this.anchor.thinkingLevel } : {}),
|
|
801
|
+
}, timeoutMs).catch((error) => {
|
|
793
802
|
if (this.turnWaiter === waiter)
|
|
794
803
|
this.turnWaiter = null;
|
|
795
804
|
this.turnIdWaiter?.resolve('');
|
|
@@ -1491,6 +1500,7 @@ class Anchor {
|
|
|
1491
1500
|
protocol: 'turns.agent-anchor.v1',
|
|
1492
1501
|
owner: 'turns.dev',
|
|
1493
1502
|
capabilities: [
|
|
1503
|
+
'model_settings',
|
|
1494
1504
|
'interrupt',
|
|
1495
1505
|
'shutdown',
|
|
1496
1506
|
'answer_interaction',
|
|
@@ -1733,8 +1743,10 @@ class Anchor {
|
|
|
1733
1743
|
cwd: path.resolve(request.cwd),
|
|
1734
1744
|
transcriptPath: path.resolve(request.transcriptPath),
|
|
1735
1745
|
workerId: String(request.workerId || ''),
|
|
1736
|
-
|
|
1737
|
-
|
|
1746
|
+
// Launch hints disappear after binding. Keep a warm session's choices
|
|
1747
|
+
// until the next explicit selection; never leak them across identities.
|
|
1748
|
+
model: String(request.model || (request.sessionId === this.sessionId ? this.model : '') || ''),
|
|
1749
|
+
thinkingLevel: String(request.thinkingLevel || (request.sessionId === this.sessionId ? this.thinkingLevel : '') || ''),
|
|
1738
1750
|
permissionMode: requestedPermissionMode(request.permissionMode),
|
|
1739
1751
|
codexHistoryMode,
|
|
1740
1752
|
idleTimeoutMs: Math.max(1_000, Number(request.idleTimeoutMs) || 300_000),
|
|
@@ -1776,6 +1788,8 @@ class Anchor {
|
|
|
1776
1788
|
if (!(this.transport instanceof ClaudeTransport) ||
|
|
1777
1789
|
this.transport.child.exitCode !== null ||
|
|
1778
1790
|
this.transport.sessionId !== this.agentSessionId ||
|
|
1791
|
+
this.transport.model !== this.model ||
|
|
1792
|
+
this.transport.thinkingLevel !== this.thinkingLevel ||
|
|
1779
1793
|
this.transport.permissionMode !== this.permissionMode) {
|
|
1780
1794
|
this.transport?.stop?.();
|
|
1781
1795
|
this.transport = new ClaudeTransport(this, createSession);
|
package/dist/harness-registry.js
CHANGED
|
@@ -372,7 +372,7 @@ export function processInvocation({ harness, sessionId, transcriptPath, cwd, pro
|
|
|
372
372
|
sessionId,
|
|
373
373
|
...(!createSession ? ['--allow-workspace-switch'] : []),
|
|
374
374
|
...(model && model !== 'auto' ? ['--model', model] : []),
|
|
375
|
-
...(thinkingLevel ? ['--reasoning-effort', thinkingLevel
|
|
375
|
+
...(thinkingLevel ? ['--reasoning-effort', thinkingLevel] : []),
|
|
376
376
|
prompt,
|
|
377
377
|
],
|
|
378
378
|
};
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import { applicationPaths, authenticate, CREDENTIAL_REFRESH_INTERVAL_MS, credentialsNeedRefresh, exchangeLegacyToken, normalizeOrigin, readCredentials, recoverRejectedCredentials, refreshCredentials, removeCredentials, uploadSocketUrl, writeCredentials, } from "./auth.js";
|
|
7
7
|
import { daemonStatus, installDaemon, uninstallDaemon } from "./daemon.js";
|
|
8
8
|
import { normalizeAllowedDirectories } from "./directory-policy.js";
|
|
9
|
+
import { logError, logInfo } from "./logging.js";
|
|
9
10
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const CLI_DIRECTORY = path.basename(HERE) === 'dist' ? path.dirname(HERE) : HERE;
|
|
11
12
|
const pkg = JSON.parse(fs.readFileSync(path.join(CLI_DIRECTORY, 'package.json'), 'utf8'));
|
|
@@ -50,11 +51,11 @@ export async function migrateLegacyCredentials(paths, origin, { legacyTokenFile
|
|
|
50
51
|
const credentials = await exchangeLegacyToken(normalizeOrigin(origin || 'https://turns.dev'), legacyToken);
|
|
51
52
|
writeCredentials(credentials, paths);
|
|
52
53
|
fs.unlinkSync(legacyTokenFile);
|
|
53
|
-
|
|
54
|
+
logInfo(`Upgraded the existing uploader credential for @${credentials.user.login}.\n`);
|
|
54
55
|
return credentials;
|
|
55
56
|
}
|
|
56
57
|
catch (error) {
|
|
57
|
-
|
|
58
|
+
logError(`Could not upgrade the existing uploader credential: ${error.message}\n`);
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -166,7 +167,7 @@ function credentialProvider(initial, { paths, interactive }) {
|
|
|
166
167
|
};
|
|
167
168
|
const timer = setInterval(() => {
|
|
168
169
|
refreshIfDue().catch((error) => {
|
|
169
|
-
|
|
170
|
+
logError(`Turns credential refresh failed: ${error.message}; retrying later.\n`);
|
|
170
171
|
});
|
|
171
172
|
}, Math.min(CREDENTIAL_REFRESH_INTERVAL_MS, 60 * 60_000));
|
|
172
173
|
timer.unref?.();
|
|
@@ -282,7 +283,7 @@ async function main(rawArguments = process.argv.slice(2)) {
|
|
|
282
283
|
credentials = await authenticate({ origin: parsed.origin, paths, interactive: false });
|
|
283
284
|
}
|
|
284
285
|
if (allowedDirectories.length) {
|
|
285
|
-
|
|
286
|
+
logInfo(`Daemon directory allowlist:\n${allowedDirectories.map((value) => ` ${value}`).join('\n')}\n`);
|
|
286
287
|
}
|
|
287
288
|
await runUpload(credentials, {
|
|
288
289
|
paths,
|
|
@@ -312,7 +313,7 @@ async function main(rawArguments = process.argv.slice(2)) {
|
|
|
312
313
|
const selected = scopedWatchOptions(command === 'upload' ? argv[1] || process.cwd() : process.cwd(), {
|
|
313
314
|
once: parsed.once,
|
|
314
315
|
});
|
|
315
|
-
|
|
316
|
+
logInfo(selected.watch
|
|
316
317
|
? `Watching sessions in ${selected.directory} as @${credentials.user.login}. Press Ctrl-C to stop.\n`
|
|
317
318
|
: `Uploading sessions in ${selected.directory} as @${credentials.user.login}.\n`);
|
|
318
319
|
await runUpload(credentials, {
|
|
@@ -330,7 +331,7 @@ try {
|
|
|
330
331
|
catch { }
|
|
331
332
|
if (invokedAsMain) {
|
|
332
333
|
main().catch((error) => {
|
|
333
|
-
|
|
334
|
+
logError(`turns: ${error?.message || error}\n`);
|
|
334
335
|
process.exitCode = 1;
|
|
335
336
|
});
|
|
336
337
|
}
|
package/dist/logging.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
/** Callers supply complete log messages, including their trailing newline. */
|
|
3
|
+
export function formatLogMessage(message, now = new Date()) {
|
|
4
|
+
const prefix = `[${now.toISOString()}] `;
|
|
5
|
+
// Timestamp every physical line, but do not add a dangling prefix after the
|
|
6
|
+
// final newline. Keep indentation and use one time for the whole message.
|
|
7
|
+
return message.replace(/^(?=[\s\S])/gmu, prefix);
|
|
8
|
+
}
|
|
9
|
+
export function logInfo(message) {
|
|
10
|
+
return process.stdout.write(formatLogMessage(message));
|
|
11
|
+
}
|
|
12
|
+
export function logError(message) {
|
|
13
|
+
return process.stderr.write(formatLogMessage(message));
|
|
14
|
+
}
|
package/dist/path-resolution.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { Transform } from 'node:stream';
|
|
5
|
+
import { logError } from "./logging.js";
|
|
5
6
|
/**
|
|
6
7
|
* Turning the paths an agent writes into things a reader can open.
|
|
7
8
|
*
|
|
@@ -526,7 +527,7 @@ export function createPathResolver({ options, machine, baseUrl, appendResolution
|
|
|
526
527
|
}
|
|
527
528
|
const chain = (chains.get(sessionId) || Promise.resolve()).then(() => resolveBatch(sessionId, workingDirectory, fresh).catch((error) => {
|
|
528
529
|
const message = error?.message || String(error);
|
|
529
|
-
|
|
530
|
+
logError(`Path resolution failed: ${redactor ? redactor.redactText(message) : message}\n`);
|
|
530
531
|
}));
|
|
531
532
|
chains.set(sessionId, chain);
|
|
532
533
|
return chain;
|
package/dist/queue-worker.js
CHANGED
|
@@ -4,6 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import { logError } from "./logging.js";
|
|
7
8
|
import { anchorId, connectExistingAgentAnchors, launchAgentAnchor, waitForAgentTurn } from "./agent-anchor-client.js";
|
|
8
9
|
import { createCodexUpdateManager } from "./codex-update.js";
|
|
9
10
|
import { isAsyncQuestionId } from "./codex-async-question-recovery.js";
|
|
@@ -384,7 +385,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
384
385
|
catch (error) {
|
|
385
386
|
const message = String(error?.message || error);
|
|
386
387
|
state.failures = (state.failures || 0) + 1;
|
|
387
|
-
|
|
388
|
+
logError(`Runtime event append failed for ${sessionId}: ${message}\n`);
|
|
388
389
|
// Socket failures are retried inside request(). Keep valid anchor
|
|
389
390
|
// events across other transient Worker/R2 failures too. Only discard
|
|
390
391
|
// a batch when the session is gone or the internally constructed row
|
|
@@ -399,7 +400,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
399
400
|
state.failures = 0;
|
|
400
401
|
}
|
|
401
402
|
else if (state.failures >= MAX_RUNTIME_FLUSH_FAILURES) {
|
|
402
|
-
|
|
403
|
+
logError(`Dropping ${events.length} undeliverable runtime events for ${sessionId} after ` +
|
|
403
404
|
`${state.failures} attempts: ${message}\n`);
|
|
404
405
|
state.events.splice(0, events.length);
|
|
405
406
|
state.failures = 0;
|
|
@@ -504,7 +505,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
504
505
|
if (state === 'idle')
|
|
505
506
|
await flushRuntime(sessionId);
|
|
506
507
|
await reportHarness(sessionId, state, entry);
|
|
507
|
-
})().catch((error) =>
|
|
508
|
+
})().catch((error) => logError(`Harness status upload failed: ${error?.message || error}\n`));
|
|
508
509
|
}
|
|
509
510
|
if (message.type === 'live_event') {
|
|
510
511
|
const turnMessageId = String(message.messageId || entry.snapshot.currentTurn?.messageId || '');
|
|
@@ -559,21 +560,25 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
559
560
|
}
|
|
560
561
|
bindAnchor(connection, snapshot);
|
|
561
562
|
}
|
|
562
|
-
const usableAnchor = async (sessionId, source, agentSessionId) => {
|
|
563
|
+
const usableAnchor = async (sessionId, source, agentSessionId, modelSettings = false) => {
|
|
563
564
|
let entry = anchors.get(sessionId);
|
|
564
565
|
if (entry && !entry.connection.closed) {
|
|
565
566
|
entry.snapshot = await entry.connection.request('snapshot', {}, 5_000).catch(() => entry.snapshot);
|
|
566
567
|
if (String(entry.snapshot.source || '') === source &&
|
|
567
|
-
String(entry.snapshot.agentSessionId || '') === agentSessionId
|
|
568
|
+
String(entry.snapshot.agentSessionId || '') === agentSessionId &&
|
|
569
|
+
(!modelSettings || entry.snapshot.capabilities?.includes('model_settings')))
|
|
568
570
|
return entry;
|
|
569
571
|
if (entry.snapshot.status === 'running')
|
|
570
|
-
throw new Error('The surviving anchor
|
|
572
|
+
throw new Error('The surviving anchor must finish its turn before it can be replaced.');
|
|
571
573
|
await entry.connection.request('shutdown', {}, 2_000).catch(() => { });
|
|
572
574
|
entry.connection.close();
|
|
573
575
|
anchors.delete(sessionId);
|
|
574
576
|
}
|
|
575
577
|
const connection = await launchAgentAnchor(sessionId);
|
|
576
578
|
entry = bindAnchor(connection, connection.snapshot || {}, sessionId);
|
|
579
|
+
if (modelSettings && !entry.snapshot.capabilities?.includes('model_settings')) {
|
|
580
|
+
throw new Error('The installed agent anchor does not support session model settings. Update the Turns CLI.');
|
|
581
|
+
}
|
|
577
582
|
await reportHarness(sessionId, 'idle', entry);
|
|
578
583
|
return entry;
|
|
579
584
|
};
|
|
@@ -629,7 +634,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
629
634
|
// Dispatch deliberately does not wait for that publish: the queue preserves
|
|
630
635
|
// order on its own, and a frame the Worker will not accept must not be able
|
|
631
636
|
// to hold a claimed prompt back from the agent.
|
|
632
|
-
void enqueueRuntime(sessionId, queuePromptBoundary(recovery?.entry || anchors.get(sessionId), claim), true).catch((error) =>
|
|
637
|
+
void enqueueRuntime(sessionId, queuePromptBoundary(recovery?.entry || anchors.get(sessionId), claim), true).catch((error) => logError(`Queue prompt boundary publish failed for ${sessionId}: ${error?.message || error}\n`));
|
|
633
638
|
let descriptor;
|
|
634
639
|
let agentSessionId = '';
|
|
635
640
|
let creating = false;
|
|
@@ -684,7 +689,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
684
689
|
// Codex installation leaves a recoverable unsent prompt in the queue.
|
|
685
690
|
if (descriptor.harness === 'codex')
|
|
686
691
|
resolveHarnessCommand('codex');
|
|
687
|
-
entry = await usableAnchor(sessionId, descriptor.harness, agentSessionId);
|
|
692
|
+
entry = await usableAnchor(sessionId, descriptor.harness, agentSessionId, Boolean(claim.session?.modelSettings));
|
|
688
693
|
}
|
|
689
694
|
if (!entry)
|
|
690
695
|
throw new Error(`No surviving agent anchor exists for ${sessionId}.`);
|
|
@@ -713,7 +718,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
713
718
|
void reportHarness(sessionId, 'stopped', entry).catch(() => { });
|
|
714
719
|
return;
|
|
715
720
|
}
|
|
716
|
-
void renewClaim().catch((error) =>
|
|
721
|
+
void renewClaim().catch((error) => logError(`Queue lease renewal failed: ${error?.message || error}\n`));
|
|
717
722
|
// Never renew lifecycle state from an old in-memory snapshot. The
|
|
718
723
|
// anchor may have disconnected since the claim began, which would
|
|
719
724
|
// otherwise resurrect a canonical stopped edge as running.
|
|
@@ -816,7 +821,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
816
821
|
queueMicrotask(() => void tryClaim(sessionId));
|
|
817
822
|
}
|
|
818
823
|
catch (error) {
|
|
819
|
-
|
|
824
|
+
logError(`Queue dispatch failed for ${sessionId}: ${error?.message || error}\n`);
|
|
820
825
|
const held = claims.get(sessionId);
|
|
821
826
|
stopClaimRenewal(held);
|
|
822
827
|
if (held?.entry && !held.entry.connection.closed) {
|
|
@@ -891,7 +896,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
891
896
|
await reportHarness(sessionId, 'stopped', held?.entry || entry).catch(() => { });
|
|
892
897
|
}
|
|
893
898
|
});
|
|
894
|
-
|
|
899
|
+
logError(`Anchor recovery failed for ${sessionId}: ${error?.message || error}\n`);
|
|
895
900
|
});
|
|
896
901
|
}
|
|
897
902
|
};
|
|
@@ -1330,7 +1335,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1330
1335
|
// as their identity: a pane already showing them recognizes the
|
|
1331
1336
|
// refresh instead of being offered its own save as a new version.
|
|
1332
1337
|
contentEtag: String(edit.afterHash || ''),
|
|
1333
|
-
}).catch((error) =>
|
|
1338
|
+
}).catch((error) => logError(`Post-edit refresh failed for ${applied.path}: ${error?.message || error}\n`));
|
|
1334
1339
|
}
|
|
1335
1340
|
replyRpc.notify({
|
|
1336
1341
|
type: 'session.file.edit.result',
|
|
@@ -1399,6 +1404,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1399
1404
|
machineId: machine,
|
|
1400
1405
|
machineHostname: os.hostname(),
|
|
1401
1406
|
capabilities: {
|
|
1407
|
+
sessionModelSettings: true,
|
|
1402
1408
|
fileSearch: true,
|
|
1403
1409
|
git: true,
|
|
1404
1410
|
fileRanges: true,
|
|
@@ -1415,7 +1421,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1415
1421
|
void tryClaim(sessionId);
|
|
1416
1422
|
}
|
|
1417
1423
|
catch (error) {
|
|
1418
|
-
|
|
1424
|
+
logError(`Queue registration failed: ${error?.message || error}\n`);
|
|
1419
1425
|
}
|
|
1420
1426
|
finally {
|
|
1421
1427
|
registering.delete(control);
|
|
@@ -1434,7 +1440,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1434
1440
|
void tryClaim(sessionId);
|
|
1435
1441
|
}
|
|
1436
1442
|
catch (error) {
|
|
1437
|
-
|
|
1443
|
+
logError(`Machine heartbeat failed: ${error?.message || error}\n`);
|
|
1438
1444
|
}
|
|
1439
1445
|
}
|
|
1440
1446
|
}
|
package/dist/upload-sessions.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import WebSocket from 'ws';
|
|
9
|
+
import { logError, logInfo } from "./logging.js";
|
|
9
10
|
import { streamDeepseekTrajectoryLines } from "./deepseek-trajectories.js";
|
|
10
11
|
import { normalizeAllowedDirectories, workingDirectoryIsAllowed } from "./directory-policy.js";
|
|
11
12
|
import { discoverGitAssociation } from "./git-association.js";
|
|
@@ -131,7 +132,7 @@ function reportingRedactor(tokens) {
|
|
|
131
132
|
return;
|
|
132
133
|
const rules = Object.fromEntries([...counts].sort((left, right) => right[1] - left[1]));
|
|
133
134
|
counts.clear();
|
|
134
|
-
|
|
135
|
+
logError(`Secret filtering applied (v${REDACTION_VERSION}): ${JSON.stringify(rules)}\n`);
|
|
135
136
|
};
|
|
136
137
|
const timer = setInterval(report, 60_000);
|
|
137
138
|
timer.unref?.();
|
|
@@ -690,7 +691,7 @@ export async function connectUploader(options, machine, reconnect = false) {
|
|
|
690
691
|
});
|
|
691
692
|
try {
|
|
692
693
|
const ready = await rpc.ready;
|
|
693
|
-
|
|
694
|
+
logInfo(`${reconnect ? 'Reconnected' : 'Connected'} to ${options.url} as @${ready.user.login} ` +
|
|
694
695
|
`from ${os.hostname()} (${machine})\n`);
|
|
695
696
|
return rpc;
|
|
696
697
|
}
|
|
@@ -700,14 +701,14 @@ export async function connectUploader(options, machine, reconnect = false) {
|
|
|
700
701
|
authenticationRecoveries += 1;
|
|
701
702
|
const credentials = await options.credentialProvider.recover();
|
|
702
703
|
options.token = credentials.accessToken;
|
|
703
|
-
|
|
704
|
+
logError('Uploader authentication was rejected; refreshed credentials.\n');
|
|
704
705
|
attempt -= 1;
|
|
705
706
|
continue;
|
|
706
707
|
}
|
|
707
708
|
if (!isSocketError(error) || attempt === MAX_SOCKET_RETRIES)
|
|
708
709
|
throw error;
|
|
709
710
|
const delay = reconnectDelay(attempt);
|
|
710
|
-
|
|
711
|
+
logError(` WebSocket connection failed: ${error.message}; retrying in ` +
|
|
711
712
|
`${(delay / 1000).toFixed(1)}s (${attempt + 1}/${MAX_SOCKET_RETRIES})\n`);
|
|
712
713
|
await wait(delay);
|
|
713
714
|
}
|
|
@@ -756,7 +757,7 @@ export class SharedUploaderConnection {
|
|
|
756
757
|
this.reconnectTimer = setTimeout(() => {
|
|
757
758
|
this.reconnectTimer = null;
|
|
758
759
|
void this.current().catch((error) => {
|
|
759
|
-
|
|
760
|
+
logError(`Shared uploader reconnect failed: ${error?.message || error}\n`);
|
|
760
761
|
this.scheduleReconnect();
|
|
761
762
|
});
|
|
762
763
|
}, 1_000);
|
|
@@ -921,7 +922,7 @@ async function uploadFile(rpc, machine, root, capture, redactor = DEFAULT_REDACT
|
|
|
921
922
|
}
|
|
922
923
|
nextRow = Number(acknowledgement.nextRow);
|
|
923
924
|
sent += batch.rows.length;
|
|
924
|
-
|
|
925
|
+
logInfo(` ${capture.relative}: ${nextRow}/${rows.length} rows · revision ${acknowledgement.revision}${acknowledgement.archived ? ' · archived' : ''}\n`);
|
|
925
926
|
}
|
|
926
927
|
return { rows: rows.length, sent, skipped: false };
|
|
927
928
|
}
|
|
@@ -1372,7 +1373,7 @@ async function reportExternalManagement(rpc, machine, descriptors, previous = ne
|
|
|
1372
1373
|
managedKeys = externallyManagedNativeDescriptorKeys(descriptors);
|
|
1373
1374
|
}
|
|
1374
1375
|
catch (error) {
|
|
1375
|
-
|
|
1376
|
+
logError(`External ownership scan failed: ${error?.message || error}\n`);
|
|
1376
1377
|
return { rpc, managed: null };
|
|
1377
1378
|
}
|
|
1378
1379
|
const observedAt = new Date().toISOString();
|
|
@@ -1391,7 +1392,7 @@ async function reportExternalManagement(rpc, machine, descriptors, previous = ne
|
|
|
1391
1392
|
});
|
|
1392
1393
|
}
|
|
1393
1394
|
catch (error) {
|
|
1394
|
-
|
|
1395
|
+
logError(`External ownership upload failed: ${error?.message || error}\n`);
|
|
1395
1396
|
return { managed: null, previous };
|
|
1396
1397
|
}
|
|
1397
1398
|
}
|
|
@@ -1408,11 +1409,11 @@ async function watchExternalManagement(rpc, machine, getDescriptors) {
|
|
|
1408
1409
|
previous = reported.previous;
|
|
1409
1410
|
if (reported.managed !== null && reported.managed !== lastManagedCount) {
|
|
1410
1411
|
lastManagedCount = reported.managed;
|
|
1411
|
-
|
|
1412
|
+
logInfo(`External ownership: ${reported.managed} managed session${reported.managed === 1 ? '' : 's'}\n`);
|
|
1412
1413
|
}
|
|
1413
1414
|
}
|
|
1414
1415
|
catch (error) {
|
|
1415
|
-
|
|
1416
|
+
logError(`External ownership watcher failed: ${error?.message || error}\n`);
|
|
1416
1417
|
}
|
|
1417
1418
|
await wait(NATIVE_WATCH_INTERVAL_MS);
|
|
1418
1419
|
}
|
|
@@ -1456,9 +1457,9 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1456
1457
|
const byHarness = new Map();
|
|
1457
1458
|
for (const session of initialSessions)
|
|
1458
1459
|
byHarness.set(session.harness, (byHarness.get(session.harness) || 0) + 1);
|
|
1459
|
-
|
|
1460
|
+
logInfo(`Native inventory: ${initialSessions.length} sessions across ${byHarness.size} harnesses\n`);
|
|
1460
1461
|
for (const [harness, count] of [...byHarness].sort())
|
|
1461
|
-
|
|
1462
|
+
logInfo(` ${harness}: ${count}\n`);
|
|
1462
1463
|
if (options.dryRun)
|
|
1463
1464
|
return;
|
|
1464
1465
|
const state = createNativeUploadState(options.nativeStateFile, machine);
|
|
@@ -1586,7 +1587,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1586
1587
|
stableSessionId: stableNativeSessionId,
|
|
1587
1588
|
redactor,
|
|
1588
1589
|
}).catch((error) => {
|
|
1589
|
-
|
|
1590
|
+
logError(`Queue worker stopped unexpectedly: ${error?.stack || error}\n`);
|
|
1590
1591
|
});
|
|
1591
1592
|
}
|
|
1592
1593
|
const totals = { current: 0, uploaded: 0, rows: 0, empty: 0, failed: 0 };
|
|
@@ -1623,7 +1624,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1623
1624
|
const message = error?.message || String(error);
|
|
1624
1625
|
if (message !== uploadProgressError) {
|
|
1625
1626
|
uploadProgressError = message;
|
|
1626
|
-
|
|
1627
|
+
logError(`Upload progress report failed: ${message}\n`);
|
|
1627
1628
|
}
|
|
1628
1629
|
}
|
|
1629
1630
|
};
|
|
@@ -1685,7 +1686,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1685
1686
|
cursor = {};
|
|
1686
1687
|
cursors.set(sessionId, cursor);
|
|
1687
1688
|
seen.delete(sessionId);
|
|
1688
|
-
|
|
1689
|
+
logInfo(` ${descriptor.harness} ${descriptor.sourcePath}: reset for ${redactionUpgrade ? 'secret-filtered' : 'full'} replay\n`);
|
|
1689
1690
|
}
|
|
1690
1691
|
result = await uploadNativeSession(rpc, machine, descriptor, cursor, (progress) => {
|
|
1691
1692
|
// Persist an explicitly incomplete fingerprint while a long replay
|
|
@@ -1693,7 +1694,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1693
1694
|
// but the empty fingerprint still schedules the unfinished tail.
|
|
1694
1695
|
state.set(sessionId, '', cursor, REDACTION_VERSION);
|
|
1695
1696
|
scheduleUploadProgress();
|
|
1696
|
-
|
|
1697
|
+
logInfo(` ${index + 1}/${changed.length} ${descriptor.harness}: ` +
|
|
1697
1698
|
`${progress.nextRow}/${progress.total} rows` +
|
|
1698
1699
|
`${progress.archived ? ' · archived' : ''}\n`);
|
|
1699
1700
|
}, redactor, {
|
|
@@ -1717,7 +1718,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1717
1718
|
failure = error;
|
|
1718
1719
|
break;
|
|
1719
1720
|
}
|
|
1720
|
-
|
|
1721
|
+
logError(` ${index + 1}/${changed.length} ${descriptor.harness}: ${error.message}; ` +
|
|
1721
1722
|
`reconnecting (${attempt + 1}/${MAX_SOCKET_RETRIES})\n`);
|
|
1722
1723
|
rpc.close();
|
|
1723
1724
|
await wait(reconnectDelay(attempt));
|
|
@@ -1734,7 +1735,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1734
1735
|
failures.delete(sessionId);
|
|
1735
1736
|
state.tombstone(sessionId);
|
|
1736
1737
|
totals.current += 1;
|
|
1737
|
-
|
|
1738
|
+
logInfo(` ${index + 1}/${changed.length} ${descriptor.harness}: deleted on turns.dev\n`);
|
|
1738
1739
|
scheduleUploadProgress();
|
|
1739
1740
|
continue;
|
|
1740
1741
|
}
|
|
@@ -1759,7 +1760,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1759
1760
|
totals.rows += result.sent;
|
|
1760
1761
|
}
|
|
1761
1762
|
if (!result.skipped || (index + 1) % 50 === 0 || index + 1 === changed.length) {
|
|
1762
|
-
|
|
1763
|
+
logInfo(` ${index + 1}/${changed.length} ${descriptor.harness} ${descriptor.sourcePath}: ` +
|
|
1763
1764
|
`${result.empty ? 'no conversation rows' : result.skipped ? `current (${result.rows} rows)` : `${result.sent} rows uploaded`}\n`);
|
|
1764
1765
|
}
|
|
1765
1766
|
scheduleUploadProgress();
|
|
@@ -1774,7 +1775,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1774
1775
|
attempts,
|
|
1775
1776
|
retryAt: Date.now() + nativeFailureRetryDelay(attempts),
|
|
1776
1777
|
});
|
|
1777
|
-
|
|
1778
|
+
logError(` ${index + 1}/${changed.length} ${descriptor.harness} ${descriptor.sourcePath}: ${error?.message || error}\n`);
|
|
1778
1779
|
scheduleUploadProgress();
|
|
1779
1780
|
}
|
|
1780
1781
|
}
|
|
@@ -1800,7 +1801,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1800
1801
|
rememberInventory(sessions);
|
|
1801
1802
|
const unsettled = changedSessions();
|
|
1802
1803
|
if (unsettled.length) {
|
|
1803
|
-
|
|
1804
|
+
logError(`${unsettled.length} native sessions kept changing; run \`turns\` without --once to follow them continuously.\n`);
|
|
1804
1805
|
}
|
|
1805
1806
|
break;
|
|
1806
1807
|
}
|
|
@@ -1816,9 +1817,9 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1816
1817
|
await reportUploadProgress(true);
|
|
1817
1818
|
rpc.close();
|
|
1818
1819
|
totals.failed = failures.size;
|
|
1819
|
-
|
|
1820
|
+
logInfo(`Native upload complete: ${JSON.stringify(totals)}\n`);
|
|
1820
1821
|
if (failures.size) {
|
|
1821
|
-
|
|
1822
|
+
logError(`${JSON.stringify({ failures: [...failures.values()] }, null, 2)}\n`);
|
|
1822
1823
|
process.exitCode = 1;
|
|
1823
1824
|
}
|
|
1824
1825
|
return;
|
|
@@ -1962,7 +1963,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1962
1963
|
shutdown();
|
|
1963
1964
|
process.exit(0);
|
|
1964
1965
|
});
|
|
1965
|
-
|
|
1966
|
+
logInfo(`Watching ${initialSessions.length} native sessions via ${nativeMonitor.count} directories; ` +
|
|
1966
1967
|
`polling known files every ${NATIVE_POLL_INTERVAL_MS / 1000}s and auditing every ` +
|
|
1967
1968
|
`${NATIVE_DISCOVERY_INTERVAL_MS / 1000}s…\n`);
|
|
1968
1969
|
let historicalSessionId = '';
|
|
@@ -1979,7 +1980,7 @@ async function uploadNative(options, machine, redactor = DEFAULT_REDACTOR) {
|
|
|
1979
1980
|
scheduleUploadProgress();
|
|
1980
1981
|
}
|
|
1981
1982
|
catch (error) {
|
|
1982
|
-
|
|
1983
|
+
logError(`Native inventory refresh failed: ${error?.message || error}\n`);
|
|
1983
1984
|
}
|
|
1984
1985
|
}
|
|
1985
1986
|
const changed = changedSessions();
|
|
@@ -2064,7 +2065,7 @@ export async function runUploader(argv = process.argv.slice(2), { credentialProv
|
|
|
2064
2065
|
const result = await uploadFile(rpc, machine, options.runs, capture, filtering.redactor);
|
|
2065
2066
|
seen.set(capture.file, fingerprint);
|
|
2066
2067
|
if (result.skipped)
|
|
2067
|
-
|
|
2068
|
+
logInfo(` ${capture.relative}: current (${result.rows} rows)\n`);
|
|
2068
2069
|
}
|
|
2069
2070
|
};
|
|
2070
2071
|
await sweep();
|
|
@@ -2072,7 +2073,7 @@ export async function runUploader(argv = process.argv.slice(2), { credentialProv
|
|
|
2072
2073
|
rpc.close();
|
|
2073
2074
|
return;
|
|
2074
2075
|
}
|
|
2075
|
-
|
|
2076
|
+
logInfo('Watching for appended capture rows…\n');
|
|
2076
2077
|
for (;;) {
|
|
2077
2078
|
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
|
2078
2079
|
await sweep();
|
|
@@ -2085,7 +2086,7 @@ export async function runUploader(argv = process.argv.slice(2), { credentialProv
|
|
|
2085
2086
|
}
|
|
2086
2087
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
2087
2088
|
runUploader().catch((error) => {
|
|
2088
|
-
|
|
2089
|
+
logError(`${error?.stack || error}\n`);
|
|
2089
2090
|
process.exitCode = 1;
|
|
2090
2091
|
});
|
|
2091
2092
|
}
|