@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.
- package/dist/agent/managedRuntimeEnvironment.js +3 -0
- package/dist/cli.js +35 -127
- package/dist/commands/executionAuditCommands.js +6 -0
- package/dist/commands/taskContextCommand.js +4 -2
- package/dist/context/sessionBootstrapManifest.js +12 -21
- package/dist/controller/clientRuntime.js +1 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +270 -162
- package/dist/controller/runtimeEventInbox.js +8 -0
- package/dist/controller/runtimeEventProcessor.js +31 -4
- package/dist/controller/runtimeHookTurnFence.js +101 -62
- package/dist/controller/runtimeLaunchCoordinator.js +38 -12
- package/dist/controller/runtimeObservationHook.js +17 -1
- package/dist/controller/structuredProviderObservation.js +39 -27
- package/dist/core/controllerClient.js +5 -0
- package/dist/core/controllerServer.js +7 -4
- package/dist/domain/agentResultTransport.js +2 -2
- package/dist/executor/agentExecutor.js +22 -38
- package/dist/executor/executorRegistry.js +16 -5
- package/dist/executor/fileRoleLaunchPlanner.js +22 -28
- package/dist/lifecycle/exactTurnTerminalization.js +3 -3
- package/dist/observability/executionAudit.js +12 -0
- package/dist/repository/executionLaneGitSnapshot.js +4 -3
- package/dist/repository/taskWorkspacePreparer.js +12 -4
- package/dist/review/taskFinalReviewContract.js +13 -32
- package/dist/runtime/agentError.js +299 -12
- package/dist/runtime/agentHost.js +419 -41
- package/dist/runtime/builtinAgentDrivers.js +5 -0
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/ports.js +16 -2
- package/dist/runtime/providerRuntimeIdentity.js +34 -28
- package/dist/runtime/runtimeCoherence.js +91 -0
- package/dist/runtime/runtimeObservation.js +8 -5
- package/dist/runtime/structuredProviderHost.js +53 -44
- package/dist/runtime/tmuxAdapters.js +72 -43
- package/dist/scheduler/activeRoleTurnDelivery.js +59 -11
- package/dist/scheduler/leaderWakeupProcessor.js +61 -7
- package/dist/storage/sqliteSchema.js +9 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/turn/turn.js +7 -1
- package/package.json +1 -1
- 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",
|
package/dist/runtime/index.js
CHANGED
|
@@ -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";
|
package/dist/runtime/ports.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
277
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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.#
|
|
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
|
-
|
|
714
|
-
|
|
734
|
+
acceptedAt: new Date().toISOString(),
|
|
735
|
+
acceptance: "transport"
|
|
715
736
|
});
|
|
716
737
|
}
|
|
717
738
|
async steerTurn(turn) {
|
|
718
|
-
|
|
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"
|
|
762
|
+
if (message.type !== "result"
|
|
764
763
|
|| optionalId(message.session_id) !== this.conversationId)
|
|
765
764
|
return;
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
784
|
+
attemptId,
|
|
776
785
|
clientOwned: true,
|
|
777
786
|
status: failed ? "failed" : "completed",
|
|
778
787
|
observedAt: new Date().toISOString(),
|