@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.
@@ -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
- // A local request can lose its response after the anchor has accepted
731
- // the prompt, so crossing this boundary permanently disables automatic
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
- if (!entry || entry.connection.closed)
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: { fileSearch: true, git: true, fileRanges: true, harnessAuth: true, codexUpdate: true },
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()],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turns/dev",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "Upload local AI agent sessions to turns.dev",
5
5
  "license": "MIT",
6
6
  "type": "module",