@turns/dev 0.0.13 → 0.0.15
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 +50 -0
- package/dist/agent-anchor-client.js +11 -2
- package/dist/agent-anchor.js +128 -6
- package/dist/agent-isolation.js +29 -0
- package/dist/codex-account.js +86 -0
- package/dist/codex-async-question-recovery.js +98 -0
- package/dist/codex-async-questions.js +84 -0
- package/dist/codex-update.js +21 -4
- package/dist/daemon.js +20 -1
- package/dist/executable-path.js +19 -0
- package/dist/file-browsing.js +17 -1
- package/dist/harness-account.js +299 -0
- package/dist/harness-auth.js +39 -6
- package/dist/harness-registry.js +32 -6
- package/dist/path-resolution.js +28 -0
- package/dist/queue-worker.js +48 -16
- package/package.json +1 -1
package/dist/queue-worker.js
CHANGED
|
@@ -6,11 +6,13 @@ import { Readable } from 'node:stream';
|
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import { anchorId, connectExistingAgentAnchors, launchAgentAnchor, waitForAgentTurn } from "./agent-anchor-client.js";
|
|
8
8
|
import { createCodexUpdateManager } from "./codex-update.js";
|
|
9
|
+
import { isAsyncQuestionId } from "./codex-async-question-recovery.js";
|
|
9
10
|
import { assertAllowedWorkingDirectory, workingDirectoryIsAllowed } from "./directory-policy.js";
|
|
10
11
|
import { browseMachinePath, streamMachineFileRange } from "./file-browsing.js";
|
|
11
12
|
import { applyFileEdit } from "./file-edits.js";
|
|
12
13
|
import { searchMachinePaths } from "./file-search.js";
|
|
13
14
|
import { readMachineGitBaseBlob, readMachineGitPatch, readMachineGitStatus } from "./git-status.js";
|
|
15
|
+
import { resolveHarnessCommand } from "./harness-registry.js";
|
|
14
16
|
import { createHarnessAuthManager } from "./harness-auth.js";
|
|
15
17
|
import { nativeDescriptorAgentSessionId, nativeDescriptorWorkingDirectory } from "./local-session-sources.js";
|
|
16
18
|
import { compactRuntimeValue } from "./native-record-compaction.js";
|
|
@@ -24,6 +26,27 @@ const MACHINE_HEARTBEAT_TIMEOUT_MS = 15_000;
|
|
|
24
26
|
const RUNTIME_BATCH_SIZE = 100;
|
|
25
27
|
const MAX_RUNTIME_FLUSH_FAILURES = 10;
|
|
26
28
|
const DEFAULT_REDACTOR = createSecretRedactor();
|
|
29
|
+
export async function answerAnchorInteraction(entry, interactionId, response) {
|
|
30
|
+
if (!entry || entry.connection.closed)
|
|
31
|
+
throw new Error('The detached harness is offline.');
|
|
32
|
+
entry.snapshot = await entry.connection.request('snapshot', {}, 5_000).catch(() => entry.snapshot);
|
|
33
|
+
const pending = entry.snapshot?.pendingInteractions?.some((interaction) => interaction.id === interactionId);
|
|
34
|
+
// Async questions can be reconstructed from the native transcript without
|
|
35
|
+
// appearing in the app-server's live/resumed item projection. Let the anchor
|
|
36
|
+
// verify and recover them instead of declaring the viewer's question stale.
|
|
37
|
+
if (!pending && !(entry.snapshot?.source === 'codex' && isAsyncQuestionId(interactionId))) {
|
|
38
|
+
throw new Error('The detached harness is no longer waiting for this interaction.');
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
await entry.connection.request('answer_interaction', { interactionId, response }, 30_000);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error?.message === 'The interaction is no longer waiting in this harness.') {
|
|
45
|
+
throw new Error('The detached harness is no longer waiting for this interaction.');
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
27
50
|
function wait(milliseconds) {
|
|
28
51
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
29
52
|
}
|
|
@@ -316,7 +339,7 @@ export function dispatchErrorIsRetryable(error) {
|
|
|
316
339
|
// OpenCode emits this only after its provider stream has exhausted its own
|
|
317
340
|
// backoff. Re-queueing the turn starts that entire retry sequence again and
|
|
318
341
|
// also persists another copy of the user's prompt in the native session.
|
|
319
|
-
return !(/(?:not supported|does not support|no resumable native session|session key.*unavailable|no local transcript descriptor|working directory (?:is (?:unavailable|not a directory|inaccessible)|must be|could not be created)|outside the daemon allowlist|agent executable not found|provider finish_reason:\s*network_error)/iu.test(message) || /attachment .* (?:size mismatch|download failed \(4\d\d\))/iu.test(message));
|
|
342
|
+
return !(/(?:not supported|does not support|no resumable native session|session key.*unavailable|no local transcript descriptor|working directory (?:is (?:unavailable|not a directory|inaccessible)|must be|could not be created)|outside the daemon allowlist|agent executable not found|backend must be upgraded|agent (?:isolation launch failed|anchor exited before opening)|provider finish_reason:\s*network_error)/iu.test(message) || /attachment .* (?:size mismatch|download failed \(4\d\d\))/iu.test(message));
|
|
320
343
|
}
|
|
321
344
|
export function queueFailureIsRetryable(error, promptStarted) {
|
|
322
345
|
return !promptStarted && dispatchErrorIsRetryable(error);
|
|
@@ -657,6 +680,10 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
657
680
|
: path.dirname(descriptor.file), options.allowedDirectories);
|
|
658
681
|
if (!fs.statSync(cwd).isDirectory())
|
|
659
682
|
throw new Error(`Working directory is unavailable: ${cwd}`);
|
|
683
|
+
// Check discovery before the durable delivery boundary, so an absent
|
|
684
|
+
// Codex installation leaves a recoverable unsent prompt in the queue.
|
|
685
|
+
if (descriptor.harness === 'codex')
|
|
686
|
+
resolveHarnessCommand('codex');
|
|
660
687
|
entry = await usableAnchor(sessionId, descriptor.harness, agentSessionId);
|
|
661
688
|
}
|
|
662
689
|
if (!entry)
|
|
@@ -727,10 +754,16 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
727
754
|
// boundary before starting this attempt so an earlier turn_error with
|
|
728
755
|
// that ID cannot immediately settle the new turn.
|
|
729
756
|
const eventCursor = entry.connection.eventCursor();
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
// replay even when the exact delivery outcome is unknown.
|
|
757
|
+
// Persist before local delivery: either acknowledgement can be lost
|
|
758
|
+
// after acceptance. Uncertain delivery must never trigger automatic replay.
|
|
733
759
|
queued.promptStarted = true;
|
|
760
|
+
await request({
|
|
761
|
+
type: 'queue.renew',
|
|
762
|
+
sessionId,
|
|
763
|
+
promptId: claim.prompt.id,
|
|
764
|
+
claimToken: claim.claimToken,
|
|
765
|
+
dispatchStarted: true,
|
|
766
|
+
});
|
|
734
767
|
await entry.connection.request('send_message', {
|
|
735
768
|
source: descriptor.harness,
|
|
736
769
|
sessionId,
|
|
@@ -771,10 +804,12 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
771
804
|
claimStarts.add(sessionId);
|
|
772
805
|
let claim;
|
|
773
806
|
try {
|
|
774
|
-
const response = await request({ type: 'queue.claim', sessionId });
|
|
807
|
+
const response = await request({ type: 'queue.claim', sessionId, dispatchTracking: true });
|
|
775
808
|
claim = response.claim;
|
|
776
809
|
if (!claim)
|
|
777
810
|
return;
|
|
811
|
+
if (!response.dispatchTracking)
|
|
812
|
+
throw new Error('The backend must be upgraded before dispatching prompts with this daemon.');
|
|
778
813
|
await runClaim(sessionId, claim);
|
|
779
814
|
// A completion can make the next item ready before its unsolicited event
|
|
780
815
|
// crosses the socket. Claim once more immediately for zero-gap draining.
|
|
@@ -986,16 +1021,7 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
986
1021
|
const sessionId = String(message.sessionId || '');
|
|
987
1022
|
const entry = anchors.get(sessionId);
|
|
988
1023
|
try {
|
|
989
|
-
|
|
990
|
-
throw new Error('The detached harness is offline.');
|
|
991
|
-
entry.snapshot = await entry.connection.request('snapshot', {}, 5_000).catch(() => entry.snapshot);
|
|
992
|
-
if (!entry.snapshot?.pendingInteractions?.some((interaction) => interaction.id === message.interactionId)) {
|
|
993
|
-
throw new Error('The detached harness is no longer waiting for this interaction.');
|
|
994
|
-
}
|
|
995
|
-
await entry.connection.request('answer_interaction', {
|
|
996
|
-
interactionId: String(message.interactionId || ''),
|
|
997
|
-
response: message.response,
|
|
998
|
-
}, 30_000);
|
|
1024
|
+
await answerAnchorInteraction(entry, String(message.interactionId || ''), message.response);
|
|
999
1025
|
await flushRuntime(sessionId);
|
|
1000
1026
|
replyRpc.notify({
|
|
1001
1027
|
type: 'session.interaction.answer.result',
|
|
@@ -1372,7 +1398,13 @@ export async function watchQueuedPrompts({ rpc, options, machine, getDescriptors
|
|
|
1372
1398
|
type: 'machine.register',
|
|
1373
1399
|
machineId: machine,
|
|
1374
1400
|
machineHostname: os.hostname(),
|
|
1375
|
-
capabilities: {
|
|
1401
|
+
capabilities: {
|
|
1402
|
+
fileSearch: true,
|
|
1403
|
+
git: true,
|
|
1404
|
+
fileRanges: true,
|
|
1405
|
+
harnessAuth: true,
|
|
1406
|
+
...(codexUpdate.available() ? { codexUpdate: true } : {}),
|
|
1407
|
+
},
|
|
1376
1408
|
workerId,
|
|
1377
1409
|
workerInstanceId,
|
|
1378
1410
|
anchorSessionIds: [...anchors.keys()],
|