@zq-silk/yui 0.15.3 → 0.15.6

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.
Files changed (41) hide show
  1. package/dist/agent/managedRuntimeEnvironment.js +3 -0
  2. package/dist/cli.js +35 -127
  3. package/dist/commands/executionAuditCommands.js +6 -0
  4. package/dist/commands/taskContextCommand.js +4 -2
  5. package/dist/context/sessionBootstrapManifest.js +12 -21
  6. package/dist/controller/clientRuntime.js +1 -1
  7. package/dist/controller/fileSchedulerStoreAdapter.js +270 -162
  8. package/dist/controller/runtimeEventInbox.js +8 -0
  9. package/dist/controller/runtimeEventProcessor.js +31 -4
  10. package/dist/controller/runtimeHookTurnFence.js +101 -62
  11. package/dist/controller/runtimeLaunchCoordinator.js +38 -12
  12. package/dist/controller/runtimeObservationHook.js +17 -1
  13. package/dist/controller/structuredProviderObservation.js +39 -27
  14. package/dist/core/controllerClient.js +5 -0
  15. package/dist/core/controllerServer.js +7 -4
  16. package/dist/domain/agentResultTransport.js +2 -2
  17. package/dist/executor/agentExecutor.js +22 -38
  18. package/dist/executor/executorRegistry.js +16 -5
  19. package/dist/executor/fileRoleLaunchPlanner.js +22 -28
  20. package/dist/lifecycle/exactTurnTerminalization.js +3 -3
  21. package/dist/observability/executionAudit.js +12 -0
  22. package/dist/repository/executionLaneGitSnapshot.js +4 -3
  23. package/dist/repository/taskWorkspacePreparer.js +12 -4
  24. package/dist/review/taskFinalReviewContract.js +13 -32
  25. package/dist/runtime/agentError.js +299 -12
  26. package/dist/runtime/agentHost.js +419 -41
  27. package/dist/runtime/builtinAgentDrivers.js +5 -0
  28. package/dist/runtime/index.js +1 -1
  29. package/dist/runtime/ports.js +16 -2
  30. package/dist/runtime/providerRuntimeIdentity.js +34 -28
  31. package/dist/runtime/runtimeCoherence.js +91 -0
  32. package/dist/runtime/runtimeObservation.js +8 -5
  33. package/dist/runtime/structuredProviderHost.js +53 -44
  34. package/dist/runtime/tmuxAdapters.js +72 -43
  35. package/dist/scheduler/activeRoleTurnDelivery.js +59 -11
  36. package/dist/scheduler/leaderWakeupProcessor.js +61 -7
  37. package/dist/storage/sqliteSchema.js +9 -0
  38. package/dist/storage/storageVersions.js +1 -1
  39. package/dist/turn/turn.js +7 -1
  40. package/package.json +1 -1
  41. package/dist/runtime/exactControlPlane.js +0 -232
@@ -70,6 +70,11 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
70
70
  adapterId: "claude",
71
71
  capabilities: Object.freeze({
72
72
  ...STRUCTURED_CLI_CAPABILITIES,
73
+ input: Object.freeze({
74
+ ...STRUCTURED_CLI_CAPABILITIES.input,
75
+ // Serialized stream input is not exact native steering.
76
+ steer: "unavailable"
77
+ }),
73
78
  observation: Object.freeze({
74
79
  ...STRUCTURED_CLI_CAPABILITIES.observation,
75
80
  sessionBootstrap: "preallocated",
@@ -5,7 +5,7 @@ export { createRuntimeBinding } from "./runtimeBinding.js";
5
5
  export { normalizeRuntimeOwner } from "./runtimeOwner.js";
6
6
  export { createSessionLaunchRequest } from "./sessionLaunchRequest.js";
7
7
  export { DEFAULT_RECENT_TURN_ID_LIMIT, hasRecentTurnId, rememberRecentTurnId, validateRecentTurnIds } from "./recentTurnIds.js";
8
- export { RuntimeGenerationMismatchError, RuntimeHostContentionError, RuntimeLaunchError } from "./ports.js";
8
+ export { promptPushOutcome, RuntimeGenerationMismatchError, RuntimeHostContentionError, RuntimeHostUnavailableError, RuntimeLaunchError } from "./ports.js";
9
9
  export { AgentHostPromptPushAdapter, TmuxSessionHost } from "./tmuxAdapters.js";
10
10
  export { FileTaskRuntimeIsolation, YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR, YUI_TASK_RUNTIME_SERVICE_NAMESPACE, assertTaskRuntimeIsolationPreflight, createTaskRuntimeIsolationDescriptor, parseTaskRuntimeIsolationDescriptor, planTaskRuntimeCleanup, taskRuntimeIsolationEnvironment, taskRuntimeIsolationFingerprint } from "./taskRuntimeIsolation.js";
11
11
  export { createSessionOwnerIdentity, discoverProviderRootByLaunchEnv, isLinuxProcessLive, listLaunchFencedProcesses, listOwnedProcessTree, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
@@ -4,8 +4,8 @@ export class RuntimeLaunchError extends Error {
4
4
  runtimeGenerationId;
5
5
  reason;
6
6
  name = "RuntimeLaunchError";
7
- constructor(retryable, runtimeGenerationId, message, reason) {
8
- super(message);
7
+ constructor(retryable, runtimeGenerationId, message, reason, options) {
8
+ super(message, options);
9
9
  this.retryable = retryable;
10
10
  this.runtimeGenerationId = runtimeGenerationId;
11
11
  this.reason = reason;
@@ -33,3 +33,17 @@ export class RuntimeGenerationMismatchError extends Error {
33
33
  this.hostState = hostState;
34
34
  }
35
35
  }
36
+ /** A reused Host reported an unusable state; this operation did not create it. */
37
+ export class RuntimeHostUnavailableError extends RuntimeLaunchError {
38
+ hostState;
39
+ constructor(runtimeGenerationId, hostState, message, options) {
40
+ super(false, runtimeGenerationId, message, undefined, options);
41
+ this.hostState = hostState;
42
+ }
43
+ }
44
+ export function promptPushOutcome(result, failure) {
45
+ return Object.freeze({
46
+ result,
47
+ ...(failure === undefined ? {} : { failure })
48
+ });
49
+ }
@@ -186,6 +186,7 @@ export function beginProviderTurn(raw, input) {
186
186
  turn: {
187
187
  ...(turnId === undefined ? {} : { turnId }),
188
188
  attemptId,
189
+ activationId: currentProviderActivation(binding).activationId,
189
190
  authorityEpoch: input.authorityEpoch,
190
191
  status: "submitting",
191
192
  submittedAt,
@@ -207,7 +208,9 @@ export function acceptProviderTurn(raw, input) {
207
208
  turn: {
208
209
  ...turn,
209
210
  status: "accepted",
210
- nativeTurnId: identity(input.nativeTurnId, "Provider native Turn id"),
211
+ ...(input.nativeTurnId === undefined
212
+ ? {}
213
+ : { nativeTurnId: identity(input.nativeTurnId, "Provider native Turn id") }),
211
214
  updatedAt: acceptedAt
212
215
  }
213
216
  });
@@ -273,8 +276,14 @@ export function settleProviderTurnSubmission(raw, input) {
273
276
  export function settleProviderTurn(raw, input) {
274
277
  const binding = validateProviderRuntimeBinding(raw);
275
278
  const turn = binding.turn;
276
- const nativeTurnId = identity(input.nativeTurnId, "Provider native Turn id");
277
- if (turn === null || turn.nativeTurnId !== nativeTurnId
279
+ const nativeTurnId = input.nativeTurnId === undefined
280
+ ? undefined : identity(input.nativeTurnId, "Provider native Turn id");
281
+ if (turn === null
282
+ || (input.attemptId === undefined
283
+ ? nativeTurnId === undefined || turn.nativeTurnId !== nativeTurnId
284
+ : turn.attemptId !== input.attemptId)
285
+ || (turn.nativeTurnId !== undefined && nativeTurnId !== undefined
286
+ && turn.nativeTurnId !== nativeTurnId)
278
287
  || turn.status !== "accepted") {
279
288
  throw new Error("Provider Turn settlement does not match the current Turn.");
280
289
  }
@@ -283,6 +292,7 @@ export function settleProviderTurn(raw, input) {
283
292
  ...binding,
284
293
  turn: {
285
294
  ...turn,
295
+ ...(nativeTurnId === undefined ? {} : { nativeTurnId }),
286
296
  status: input.status,
287
297
  updatedAt: settledAt,
288
298
  ...(input.reason === undefined
@@ -301,33 +311,25 @@ export function updateProviderConversationRecoverability(raw, recoverability) {
301
311
  : entry)
302
312
  });
303
313
  }
314
+ /** Shared pre-start and commit guard for explicit native Conversation replacement. */
315
+ export function assertProviderConversationReplaceable(raw) {
316
+ const binding = validateProviderRuntimeBinding(raw);
317
+ if (currentProviderActivation(binding) !== null || binding.authority.owner !== "none") {
318
+ throw new Error("Provider Conversation replacement requires its prior Activation to be ended and unowned.");
319
+ }
320
+ if (providerTurnIsActive(binding.turn)) {
321
+ throw new Error(`Provider Conversation replacement cannot discard unsettled input attempt ${binding.turn.attemptId} (${binding.turn.status}). Resolve its actual outcome before selecting a new Conversation.`);
322
+ }
323
+ }
304
324
  export function supersedeProviderConversation(raw, input) {
305
325
  const binding = validateProviderRuntimeBinding(raw);
306
326
  const current = currentProviderConversation(binding);
307
- const basis = input.basis;
308
- if (basis !== "terminal-session") {
327
+ if (input.basis !== "terminal-session") {
309
328
  throw new Error("Provider Conversation replacement basis is invalid.");
310
329
  }
330
+ assertProviderConversationReplaceable(binding);
311
331
  const switchedAt = timestamp(input.switchedAt, "Provider Conversation replacement timestamp");
312
332
  const epoch = current.epoch + 1;
313
- const terminalReason = "terminal-session-replaced";
314
- const activations = binding.activations.map((entry) => entry.status === "active"
315
- ? {
316
- ...entry,
317
- status: "failed",
318
- endedAt: switchedAt,
319
- terminalReason
320
- }
321
- : entry);
322
- const turn = binding.turn !== null
323
- && ["submitting", "accepted", "delivery-unknown"].includes(binding.turn.status)
324
- ? {
325
- ...binding.turn,
326
- status: binding.turn.nativeTurnId === undefined ? "rejected" : "failed",
327
- updatedAt: switchedAt,
328
- terminalReason
329
- }
330
- : binding.turn;
331
333
  return validateProviderRuntimeBinding({
332
334
  ...binding,
333
335
  currentConversationEpoch: epoch,
@@ -343,7 +345,7 @@ export function supersedeProviderConversation(raw, input) {
343
345
  createdAt: switchedAt
344
346
  }
345
347
  ],
346
- activations: [...activations, {
348
+ activations: [...binding.activations, {
347
349
  activationId: identity(input.activationId, "Provider Activation id"),
348
350
  conversationId: input.conversationId,
349
351
  generation: 1,
@@ -356,7 +358,6 @@ export function supersedeProviderConversation(raw, input) {
356
358
  holderId: input.activationId,
357
359
  changedAt: switchedAt
358
360
  },
359
- turn,
360
361
  goal: null
361
362
  });
362
363
  }
@@ -464,8 +465,12 @@ export function validateProviderRuntimeBinding(value) {
464
465
  }
465
466
  if (!Object.hasOwn(value, "turn"))
466
467
  throw new Error("Provider Runtime Binding requires Turn state.");
467
- if (value.turn !== null)
468
+ if (value.turn !== null) {
468
469
  validateProviderTurn(value.turn, value.authority.epoch);
470
+ if (value.turn.activationId !== undefined && !activationIds.has(value.turn.activationId)) {
471
+ throw new Error("Provider Turn references an unknown original Activation.");
472
+ }
473
+ }
469
474
  if (!Object.hasOwn(value, "goal"))
470
475
  throw new Error("Provider Runtime Binding requires Goal state.");
471
476
  if (value.goal !== null)
@@ -515,9 +520,10 @@ function validateProviderTurn(turn, currentAuthorityEpoch) {
515
520
  }
516
521
  const hasAcceptedIdentity = turn.status === "accepted"
517
522
  || turn.status === "completed" || turn.status === "failed" || turn.status === "cancelled";
518
- if (hasAcceptedIdentity)
523
+ if (hasAcceptedIdentity && turn.nativeTurnId !== undefined) {
519
524
  identity(turn.nativeTurnId, "Provider native Turn id");
520
- else if (turn.nativeTurnId !== undefined) {
525
+ }
526
+ else if (!hasAcceptedIdentity && turn.nativeTurnId !== undefined) {
521
527
  throw new Error("Unaccepted Provider Turn cannot have a native Turn id.");
522
528
  }
523
529
  }
@@ -0,0 +1,91 @@
1
+ import { resolve } from "node:path";
2
+ import { callController as defaultCallController } from "../core/controllerClient.js";
3
+ import { inspectStorageSchema } from "../storage/storageSchema.js";
4
+ import { yuiVersionIdentity } from "../version.js";
5
+ export async function assertRuntimeCoherence(input, options = {}) {
6
+ const home = resolve(input.actualHome);
7
+ const identity = validateVersionIdentity(options.identity ?? yuiVersionIdentity());
8
+ const storage = (options.inspectStorage ?? inspectStorageSchema)(home);
9
+ if (storage.status !== "current") {
10
+ throw new Error(`Managed control-plane storage is not current: ${storage.status}.`);
11
+ }
12
+ if (storage.currentVersion !== identity.storageVersion) {
13
+ throw new Error("Managed control-plane storage version is incompatible "
14
+ + `(expected ${identity.storageVersion}, found `
15
+ + `${storage.currentVersion ?? "unknown"}).`);
16
+ }
17
+ if (options.checkController !== false) {
18
+ const call = options.callController ?? defaultCallController;
19
+ try {
20
+ const status = await call(home, "controller.status", {});
21
+ assertControllerContinuityIdentity(status, identity);
22
+ }
23
+ catch (error) {
24
+ if (!isDefinitelyNotRunning(error))
25
+ throw error;
26
+ }
27
+ }
28
+ return identity;
29
+ }
30
+ export function assertControllerStatusIdentity(status, expected = yuiVersionIdentity()) {
31
+ if (!isRecord(status) || status.running !== true) {
32
+ throw new Error("Controller status does not describe a running Controller.");
33
+ }
34
+ assertControllerField(status.protocolVersion, expected.controllerProtocolVersion, "protocol");
35
+ assertControllerField(status.version, expected.version, "version");
36
+ assertControllerField(status.storageVersion, expected.storageVersion, "storage version");
37
+ assertControllerField(status.minimumStorageVersion, expected.minimumStorageVersion, "minimum storage migration version");
38
+ }
39
+ function validateVersionIdentity(value) {
40
+ if (!isRecord(value))
41
+ throw new Error("Yui version identity is invalid.");
42
+ const version = requireText(value.version, "Yui version");
43
+ const controllerProtocolVersion = requireVersion(value.controllerProtocolVersion, "Controller protocol version");
44
+ const storageVersion = requireVersion(value.storageVersion, "Storage version");
45
+ const minimumStorageVersion = requireVersion(value.minimumStorageVersion, "Minimum storage migration version");
46
+ if (minimumStorageVersion > storageVersion) {
47
+ throw new Error("Minimum storage migration version cannot exceed the current storage version.");
48
+ }
49
+ return {
50
+ version,
51
+ controllerProtocolVersion,
52
+ storageVersion,
53
+ minimumStorageVersion
54
+ };
55
+ }
56
+ function assertControllerContinuityIdentity(status, expected) {
57
+ if (!isRecord(status) || status.running !== true) {
58
+ throw new Error("Controller status does not describe a running Controller.");
59
+ }
60
+ if (typeof status.version !== "string" || status.version.trim().length === 0) {
61
+ throw new Error("Controller version is invalid at the managed continuity gate.");
62
+ }
63
+ assertControllerField(status.protocolVersion, expected.controllerProtocolVersion, "protocol");
64
+ assertControllerField(status.storageVersion, expected.storageVersion, "storage version");
65
+ }
66
+ function assertControllerField(actual, expected, label) {
67
+ if (actual !== expected) {
68
+ throw new Error(`Controller ${label} is incompatible with the exact control plane `
69
+ + `(expected ${expected}, found ${typeof actual === "string" || typeof actual === "number" ? actual : "unknown"}). `
70
+ + "Run controller restart through the matching exact control-plane invocation "
71
+ + "before writing new Task records.");
72
+ }
73
+ }
74
+ function requireText(value, label) {
75
+ if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
76
+ throw new Error(`${label} is invalid.`);
77
+ }
78
+ return value;
79
+ }
80
+ function requireVersion(value, label) {
81
+ if (!Number.isSafeInteger(value) || value < 1) {
82
+ throw new Error(`${label} is invalid.`);
83
+ }
84
+ return value;
85
+ }
86
+ function isRecord(value) {
87
+ return typeof value === "object" && value !== null && !Array.isArray(value);
88
+ }
89
+ function isDefinitelyNotRunning(error) {
90
+ return isRecord(error) && error.code === "CONTROLLER_NOT_RUNNING";
91
+ }
@@ -97,15 +97,18 @@ export function createRuntimeObservation(input) {
97
97
  }
98
98
  if (PROVIDER_STATE.has(input.kind)
99
99
  && input.authority !== "provider-structured"
100
- && input.authority !== "controller") {
100
+ && input.authority !== "controller"
101
+ && !(input.kind === "turn.accepted" && input.authority === "transport"
102
+ && input.fence.receiptId !== undefined)) {
101
103
  throw new Error(`${input.kind} requires provider-structured or controller authority.`);
102
104
  }
103
105
  const fence = normalizeFence(input.fence);
104
106
  if (TURN_SCOPED.has(input.kind) && fence.nativeSessionId === undefined) {
105
107
  throw new Error(`${input.kind} requires nativeSessionId.`);
106
108
  }
107
- if (TURN_SCOPED.has(input.kind) && fence.nativeTurnId === undefined) {
108
- throw new Error(`${input.kind} requires nativeTurnId.`);
109
+ if (TURN_SCOPED.has(input.kind) && fence.nativeTurnId === undefined
110
+ && fence.receiptId === undefined) {
111
+ throw new Error(`${input.kind} requires a native Turn or exact receipt identity.`);
109
112
  }
110
113
  if ((input.kind.startsWith("activation.") || CONTINUATION_SCOPED.has(input.kind)
111
114
  || input.kind === "native-work.snapshot")
@@ -473,7 +476,7 @@ export function runtimeObservationSemanticKey(input) {
473
476
  return [
474
477
  "terminal",
475
478
  ...continuationIdentity,
476
- fence.continuationId ?? fence.nativeTurnId ?? "none",
479
+ fence.continuationId ?? fence.receiptId ?? fence.nativeTurnId ?? "none",
477
480
  input.kind,
478
481
  input.payload?.outcome ?? input.payload?.failure?.error.code ?? "terminal",
479
482
  input.kind === "continuation.settled" ? input.payload?.resultRef ?? "none" : "none",
@@ -517,7 +520,7 @@ export function runtimeObservationSemanticKey(input) {
517
520
  fence.driverId,
518
521
  fence.conversationId ?? fence.nativeSessionId ?? fence.runtimeGenerationId,
519
522
  fence.activationId ?? fence.runtimeGenerationId,
520
- fence.continuationId ?? fence.nativeTurnId ?? "none",
523
+ fence.continuationId ?? fence.receiptId ?? fence.nativeTurnId ?? "none",
521
524
  fence.continuationGeneration ?? "none",
522
525
  input.sequence,
523
526
  input.kind
@@ -11,16 +11,19 @@ const CODEX_PROXY_HANDSHAKE_TIMEOUT_MS = 10_000;
11
11
  export class ProviderDeliveryUnknownError extends Error {
12
12
  attemptId;
13
13
  name = "ProviderDeliveryUnknownError";
14
- constructor(message, attemptId) {
15
- super(message);
14
+ constructor(message, attemptId,
15
+ // Wrapping must not become the end of the causal chain: the original
16
+ // transport or Controller failure is the reason a reader needs.
17
+ options) {
18
+ super(message, options);
16
19
  this.attemptId = attemptId;
17
20
  }
18
21
  }
19
22
  export class ProviderTurnRejectedError extends Error {
20
23
  attemptId;
21
24
  name = "ProviderTurnRejectedError";
22
- constructor(message, attemptId) {
23
- super(message);
25
+ constructor(message, attemptId, options) {
26
+ super(message, options);
24
27
  this.attemptId = attemptId;
25
28
  }
26
29
  }
@@ -381,6 +384,7 @@ class CodexStructuredProviderSession {
381
384
  adapterId = "codex";
382
385
  #activeTurnId;
383
386
  #clientOwnedTurnId;
387
+ #clientOwnedAttemptId;
384
388
  #submissionPending = false;
385
389
  #bufferedStarts = [];
386
390
  #bufferedTerminals = [];
@@ -443,6 +447,7 @@ class CodexStructuredProviderSession {
443
447
  session.#activeTurnId = resumedActiveTurnId;
444
448
  const ownedTurn = control.kind === "restore" ? control.ownedTurn : undefined;
445
449
  session.#clientOwnedTurnId = ownedTurn?.turnId;
450
+ session.#clientOwnedAttemptId = ownedTurn?.attemptId;
446
451
  stopOpeningBuffer();
447
452
  let recoveredTerminal;
448
453
  for (const message of openingMessages) {
@@ -463,6 +468,7 @@ class CodexStructuredProviderSession {
463
468
  conversationId,
464
469
  nativeSessionId: conversationId,
465
470
  nativeTurnId: ownedTurn.turnId,
471
+ attemptId: ownedTurn.attemptId,
466
472
  clientOwned: true,
467
473
  status: recovered.status === "failed"
468
474
  ? "failed"
@@ -477,6 +483,7 @@ class CodexStructuredProviderSession {
477
483
  })
478
484
  };
479
485
  session.#clientOwnedTurnId = undefined;
486
+ session.#clientOwnedAttemptId = undefined;
480
487
  }
481
488
  else {
482
489
  throw new ProviderDeliveryUnknownError(`Codex resume could not recover the persisted Yui Turn ${ownedTurn.turnId}.`, ownedTurn.attemptId);
@@ -579,12 +586,14 @@ class CodexStructuredProviderSession {
579
586
  }
580
587
  this.#activeTurnId = acceptance.turnId;
581
588
  this.#clientOwnedTurnId = acceptance.turnId;
589
+ this.#clientOwnedAttemptId = turn.attemptId;
582
590
  return Object.freeze({
583
591
  attemptId: turn.attemptId,
584
592
  conversationId: this.conversationId,
585
593
  nativeSessionId: this.conversationId,
586
594
  nativeTurnId: acceptance.turnId,
587
- acceptedAt: new Date().toISOString()
595
+ acceptedAt: new Date().toISOString(),
596
+ acceptance: "provider"
588
597
  });
589
598
  }
590
599
  finally {
@@ -619,7 +628,8 @@ class CodexStructuredProviderSession {
619
628
  conversationId: this.conversationId,
620
629
  nativeSessionId: this.conversationId,
621
630
  nativeTurnId,
622
- acceptedAt: new Date().toISOString()
631
+ acceptedAt: new Date().toISOString(),
632
+ acceptance: "provider"
623
633
  });
624
634
  }
625
635
  #emitTerminal(terminal) {
@@ -632,12 +642,20 @@ class CodexStructuredProviderSession {
632
642
  });
633
643
  }
634
644
  #completeTerminal(terminal, emit) {
635
- const clientOwned = terminal.nativeTurnId === this.#clientOwnedTurnId;
645
+ const clientOwned = terminal.nativeTurnId !== undefined
646
+ && terminal.nativeTurnId === this.#clientOwnedTurnId;
647
+ const attemptId = clientOwned ? this.#clientOwnedAttemptId : undefined;
636
648
  if (terminal.nativeTurnId === this.#activeTurnId)
637
649
  this.#activeTurnId = undefined;
638
- if (clientOwned)
650
+ if (clientOwned) {
639
651
  this.#clientOwnedTurnId = undefined;
640
- const completed = { ...terminal, clientOwned };
652
+ this.#clientOwnedAttemptId = undefined;
653
+ }
654
+ const completed = {
655
+ ...terminal,
656
+ clientOwned,
657
+ ...(attemptId === undefined ? {} : { attemptId })
658
+ };
641
659
  if (emit)
642
660
  this.onTerminal?.(completed);
643
661
  return completed;
@@ -657,7 +675,8 @@ class ClaudeStructuredProviderSession {
657
675
  channel;
658
676
  onGoal;
659
677
  adapterId = "claude";
660
- #activeTurnId;
678
+ #activeAttemptId;
679
+ #resultAttempts = new Map();
661
680
  #lastGoalKey;
662
681
  constructor(child, exit, processInstanceId, conversationId, channel, onGoal) {
663
682
  this.child = child;
@@ -681,12 +700,16 @@ class ClaudeStructuredProviderSession {
681
700
  return this.conversationId;
682
701
  }
683
702
  get activeTurnId() {
684
- return this.#activeTurnId;
703
+ // stream-json result.uuid is a message identity, not an execution identity.
704
+ return undefined;
685
705
  }
686
706
  async submitTurn(turn) {
687
- if (this.#activeTurnId !== undefined) {
707
+ if (this.#activeAttemptId !== undefined) {
688
708
  throw new ProviderTurnRejectedError("Provider Conversation already has an unsettled Turn.", turn.attemptId);
689
709
  }
710
+ // Reserve before the pipe write: a fast result can precede its callback.
711
+ // A failed write is ambiguous and must retain the same occupancy.
712
+ this.#activeAttemptId = turn.attemptId;
690
713
  try {
691
714
  await this.channel.send({
692
715
  type: "user",
@@ -697,47 +720,23 @@ class ClaudeStructuredProviderSession {
697
720
  });
698
721
  }
699
722
  catch (error) {
700
- throw new ProviderDeliveryUnknownError(`Claude input write did not complete: ${error instanceof Error ? error.message : String(error)}`, turn.attemptId);
723
+ throw new ProviderDeliveryUnknownError(`Claude input write did not complete: ${error instanceof Error ? error.message : String(error)}`, turn.attemptId, { cause: error });
701
724
  }
702
725
  // AgentHost is the sole writer to this dedicated stream-json process.
703
726
  // A completed pipe write is the smallest reliable acceptance boundary;
704
727
  // Claude's later `result` is the matching terminal for this serialized
705
728
  // Turn. Requiring an echoed user-message creates a second, brittle
706
729
  // protocol without improving delivery safety.
707
- const nativeTurnId = `claude-stream:${turn.attemptId}`;
708
- this.#activeTurnId = nativeTurnId;
709
730
  return Object.freeze({
710
731
  attemptId: turn.attemptId,
711
732
  conversationId: this.conversationId,
712
733
  nativeSessionId: this.conversationId,
713
- nativeTurnId,
714
- acceptedAt: new Date().toISOString()
734
+ acceptedAt: new Date().toISOString(),
735
+ acceptance: "transport"
715
736
  });
716
737
  }
717
738
  async steerTurn(turn) {
718
- const nativeTurnId = this.#activeTurnId;
719
- if (nativeTurnId === undefined) {
720
- throw new ProviderTurnRejectedError("Provider Conversation has no active Turn to steer.", turn.attemptId);
721
- }
722
- try {
723
- await this.channel.send({
724
- type: "user",
725
- message: {
726
- role: "user",
727
- content: [{ type: "text", text: turn.boundedText }]
728
- }
729
- });
730
- }
731
- catch (error) {
732
- throw new ProviderDeliveryUnknownError(`Claude steer write did not complete: ${error instanceof Error ? error.message : String(error)}`, turn.attemptId);
733
- }
734
- return Object.freeze({
735
- attemptId: turn.attemptId,
736
- conversationId: this.conversationId,
737
- nativeSessionId: this.conversationId,
738
- nativeTurnId,
739
- acceptedAt: new Date().toISOString()
740
- });
739
+ throw new ProviderTurnRejectedError("Claude stream-json does not expose an exact native Turn identity for steering.", turn.attemptId);
741
740
  }
742
741
  waitForExit() {
743
742
  return this.exit;
@@ -760,11 +759,21 @@ class ClaudeStructuredProviderSession {
760
759
  }
761
760
  if (message.type === "user")
762
761
  return;
763
- if (message.type !== "result" || this.#activeTurnId === undefined
762
+ if (message.type !== "result"
764
763
  || optionalId(message.session_id) !== this.conversationId)
765
764
  return;
766
- const nativeTurnId = this.#activeTurnId;
767
- this.#activeTurnId = undefined;
765
+ // A result UUID identifies this message, not the Provider execution.
766
+ // Retain its local association so a delayed duplicate cannot be assigned
767
+ // to a successor request on the same long-lived process.
768
+ const resultId = optionalId(message.uuid);
769
+ const priorAttempt = resultId === undefined ? undefined : this.#resultAttempts.get(resultId);
770
+ const attemptId = priorAttempt ?? this.#activeAttemptId;
771
+ if (attemptId === undefined)
772
+ return;
773
+ if (resultId !== undefined)
774
+ this.#resultAttempts.set(resultId, attemptId);
775
+ if (attemptId === this.#activeAttemptId)
776
+ this.#activeAttemptId = undefined;
768
777
  const failed = message.is_error === true || message.subtype === "error_during_execution";
769
778
  const result = typeof message.result === "string" && message.result.length > 0
770
779
  ? message.result
@@ -772,7 +781,7 @@ class ClaudeStructuredProviderSession {
772
781
  onTerminal?.({
773
782
  conversationId: this.conversationId,
774
783
  nativeSessionId: this.conversationId,
775
- nativeTurnId,
784
+ attemptId,
776
785
  clientOwned: true,
777
786
  status: failed ? "failed" : "completed",
778
787
  observedAt: new Date().toISOString(),