@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
|
@@ -39,6 +39,22 @@ export class FileRuntimeEventProcessor {
|
|
|
39
39
|
offset = wave.nextOffset;
|
|
40
40
|
if (wave.candidates.length === 0)
|
|
41
41
|
continue;
|
|
42
|
+
if (ownsResultTransaction(wave.candidates[0].event)) {
|
|
43
|
+
// Terminal preparation may inspect external workspace resources.
|
|
44
|
+
// Its observer owns the atomic result commit after that inspection;
|
|
45
|
+
// never enclose preparation in the cross-Task batch transaction.
|
|
46
|
+
const candidate = wave.candidates[0];
|
|
47
|
+
try {
|
|
48
|
+
stateTransactions += 1;
|
|
49
|
+
const failure = this.finalizeOne(this.foldOne(candidate, now), acknowledgedEventIds, deferred);
|
|
50
|
+
if (failure !== undefined)
|
|
51
|
+
recordDrainFailure(failure, failed, failedTaskIds);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
recordDrainFailure(candidateDrainFailure(candidate.event, error), failed, failedTaskIds);
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
42
58
|
try {
|
|
43
59
|
stateTransactions += 1;
|
|
44
60
|
const folded = this.observer.withRuntimeEventTransaction(() => (wave.candidates.map((candidate) => this.foldOne(candidate, now))));
|
|
@@ -131,8 +147,9 @@ export class FileRuntimeEventProcessor {
|
|
|
131
147
|
if (taskId !== undefined) {
|
|
132
148
|
const task = this.observer.getTask(taskId);
|
|
133
149
|
if (task === null
|
|
134
|
-
|| task.status
|
|
135
|
-
||
|
|
150
|
+
|| task.status === "archived"
|
|
151
|
+
|| (!["turn.completed", "turn.failed", "turn.cancelled"].includes(event.observation.kind)
|
|
152
|
+
&& (task.status !== "active" || task.executionGate.state !== "enabled")))
|
|
136
153
|
return "obsolete";
|
|
137
154
|
}
|
|
138
155
|
return this.observer.observeRuntimeObservation?.(event.observation, now) ?? "obsolete";
|
|
@@ -452,15 +469,24 @@ function selectTaskOrderedWave(candidates, offset, failedTaskIds) {
|
|
|
452
469
|
nextOffset += 1;
|
|
453
470
|
continue;
|
|
454
471
|
}
|
|
472
|
+
if (ownsResultTransaction(candidate.event) && selected.length > 0)
|
|
473
|
+
break;
|
|
455
474
|
if (taskId !== undefined && selectedTaskIds.has(taskId))
|
|
456
475
|
break;
|
|
457
476
|
selected.push(candidate);
|
|
458
477
|
if (taskId !== undefined)
|
|
459
478
|
selectedTaskIds.add(taskId);
|
|
460
479
|
nextOffset += 1;
|
|
480
|
+
if (ownsResultTransaction(candidate.event))
|
|
481
|
+
break;
|
|
461
482
|
}
|
|
462
483
|
return { candidates: selected, nextOffset };
|
|
463
484
|
}
|
|
485
|
+
function ownsResultTransaction(event) {
|
|
486
|
+
return event.type === "native-turn-terminal"
|
|
487
|
+
|| (event.type === "runtime-observation"
|
|
488
|
+
&& ["turn.completed", "turn.failed", "turn.cancelled"].includes(event.observation.kind));
|
|
489
|
+
}
|
|
464
490
|
function recordDrainFailure(failure, failed, failedTaskIds) {
|
|
465
491
|
failed.push(failure);
|
|
466
492
|
if (failure.scope === "task")
|
|
@@ -588,8 +614,9 @@ export class AsyncRuntimeEventProcessor {
|
|
|
588
614
|
if (taskId !== undefined) {
|
|
589
615
|
const task = await this.observer.getTask(taskId);
|
|
590
616
|
if (task === null
|
|
591
|
-
|| task.status
|
|
592
|
-
||
|
|
617
|
+
|| task.status === "archived"
|
|
618
|
+
|| (!["turn.completed", "turn.failed", "turn.cancelled"].includes(event.observation.kind)
|
|
619
|
+
&& (task.status !== "active" || task.executionGate.state !== "enabled")))
|
|
593
620
|
return "obsolete";
|
|
594
621
|
}
|
|
595
622
|
const outcome = (await this.observer.observeRuntimeObservation?.(event.observation, now))
|
|
@@ -26,22 +26,56 @@ export function resolveRuntimeHookTurnFence(environment, adapterId, payloadNativ
|
|
|
26
26
|
const nativeSessionId = requireIdentity(payloadNativeSessionId, "Provider session id");
|
|
27
27
|
const store = openCurrentTaskStore(home);
|
|
28
28
|
const task = store.getTask(taskId);
|
|
29
|
-
if (task === null
|
|
30
|
-
|| (!(task.status === "active" && task.executionGate.state === "enabled")
|
|
31
|
-
&& !(task.status === "completed" && (options.terminal === true || options.sessionOnly === true)))) {
|
|
29
|
+
if (task === null) {
|
|
32
30
|
throw new Error("Runtime observation Hook Task does not accept this lifecycle boundary.");
|
|
33
31
|
}
|
|
34
32
|
const role = store.getRole(taskId, roleName);
|
|
35
|
-
if (role === null || role.activeAgentId !== agentId) {
|
|
36
|
-
throw new Error("Runtime observation Hook Role or Agent is not current.");
|
|
37
|
-
}
|
|
38
33
|
const sessions = store.getTaskRoleSessionSet(taskId, roleName);
|
|
39
|
-
if (sessions !== null && sessions.activeAgentId !== agentId) {
|
|
40
|
-
throw new Error("Runtime observation Hook Session Agent is not current.");
|
|
41
|
-
}
|
|
42
34
|
const session = sessions?.sessions[agentId];
|
|
35
|
+
const executionSession = session?.nativeSessionId === nativeSessionId
|
|
36
|
+
? session
|
|
37
|
+
: sessions?.history?.find((entry) => (entry.agentId === agentId && entry.nativeSessionId === nativeSessionId));
|
|
43
38
|
const activeTurn = store.getActiveTurn(taskId, roleName);
|
|
44
39
|
const providerTurn = sessions?.providerBinding?.turn;
|
|
40
|
+
// An input belongs to the Activation recorded at registration, even after
|
|
41
|
+
// detach or a successor Host launch. "Current activation" is not its owner.
|
|
42
|
+
const activation = providerTurn?.activationId === undefined
|
|
43
|
+
? undefined
|
|
44
|
+
: sessions?.providerBinding?.activations.find((entry) => entry.activationId === providerTurn.activationId);
|
|
45
|
+
const matchesProviderTurn = providerTurn !== null
|
|
46
|
+
&& providerTurn !== undefined
|
|
47
|
+
&& executionSession?.adapterId === adapterId
|
|
48
|
+
&& executionSession.effective.workspace.root === workspace
|
|
49
|
+
&& activation?.conversationId === nativeSessionId
|
|
50
|
+
&& ((options.attemptId !== undefined && providerTurn.attemptId === options.attemptId)
|
|
51
|
+
|| (options.nativeTurnId !== undefined && providerTurn.nativeTurnId === options.nativeTurnId))
|
|
52
|
+
&& (options.attemptId === undefined || providerTurn.attemptId === options.attemptId)
|
|
53
|
+
&& (options.nativeTurnId === undefined || providerTurn.nativeTurnId === undefined
|
|
54
|
+
|| providerTurn.nativeTurnId === options.nativeTurnId);
|
|
55
|
+
const acceptedTurn = acceptedTurnBinding(store.listEvents(taskId), {
|
|
56
|
+
taskId, roleName, agentId, nativeSessionId,
|
|
57
|
+
...(options.nativeTurnId === undefined ? {} : { nativeTurnId: options.nativeTurnId }),
|
|
58
|
+
...(options.attemptId === undefined ? {} : { attemptId: options.attemptId })
|
|
59
|
+
});
|
|
60
|
+
const acceptedBinding = acceptedTurn ?? (options.continuationId === undefined ? null : knownContinuationBinding(store.listEvents(taskId), {
|
|
61
|
+
taskId, roleName, agentId, nativeSessionId,
|
|
62
|
+
continuationId: options.continuationId,
|
|
63
|
+
continuationGeneration: options.continuationGeneration ?? 1
|
|
64
|
+
}));
|
|
65
|
+
// Collection of an exactly accepted terminal fact is not a new action by
|
|
66
|
+
// the Role. A successor's active Agent or revoked execution permission
|
|
67
|
+
// cannot erase the original Turn's evidence.
|
|
68
|
+
const existingExecutionObservation = (options.terminal === true || options.attemptId !== undefined)
|
|
69
|
+
&& (acceptedBinding !== null || matchesProviderTurn);
|
|
70
|
+
if (!(task.status === "active" && task.executionGate.state === "enabled")
|
|
71
|
+
&& !(task.status === "completed" && options.sessionOnly === true)
|
|
72
|
+
&& !existingExecutionObservation) {
|
|
73
|
+
throw new Error("Runtime observation Hook Task does not accept this lifecycle boundary.");
|
|
74
|
+
}
|
|
75
|
+
if (!existingExecutionObservation && (role === null || role.activeAgentId !== agentId
|
|
76
|
+
|| (sessions !== null && sessions.activeAgentId !== agentId))) {
|
|
77
|
+
throw new Error("Runtime observation Hook Role or Agent is not current.");
|
|
78
|
+
}
|
|
45
79
|
// Launch generation and Turn are durable facts, never envelope facts. A
|
|
46
80
|
// native pane outlives both, so anything it inherited at launch is stale for
|
|
47
81
|
// every later generation; the Session's own record (or the in-flight launch
|
|
@@ -54,22 +88,25 @@ export function resolveRuntimeHookTurnFence(environment, adapterId, payloadNativ
|
|
|
54
88
|
const reservedRuntimeGenerationId = isRuntimeLaunchReservation(lifecycleMailbox?.processing)
|
|
55
89
|
? lifecycleMailbox?.processing?.batchId
|
|
56
90
|
: undefined;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
&&
|
|
63
|
-
&&
|
|
64
|
-
|
|
65
|
-
|
|
91
|
+
// Startup has a fresh process envelope and must prove the exact persisted
|
|
92
|
+
// launch. Existing/late execution events instead retain their old binding.
|
|
93
|
+
const reservedStartup = options.startupSession !== undefined
|
|
94
|
+
&& activeTurn !== null
|
|
95
|
+
&& reservedRuntimeGenerationId !== undefined
|
|
96
|
+
&& environment.YUI_RUNTIME_GENERATION_ID === reservedRuntimeGenerationId
|
|
97
|
+
&& !hasRuntimeCleanupObligation(lifecycleMailbox);
|
|
98
|
+
const runtimeGenerationId = requireIdentity(acceptedBinding?.fence.runtimeGenerationId
|
|
99
|
+
?? (matchesProviderTurn ? activation.activationId : undefined)
|
|
100
|
+
?? (reservedStartup ? reservedRuntimeGenerationId : session?.runtimeGenerationId), "Runtime generation id");
|
|
101
|
+
const directProviderTurn = matchesProviderTurn && providerTurn.turnId === undefined;
|
|
66
102
|
const sessionOnlyObservation = options.sessionOnly === true && activeTurn === null;
|
|
67
|
-
if (directProviderTurn || sessionOnlyObservation) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
||
|
|
71
|
-
||
|
|
72
|
-
||
|
|
103
|
+
if (acceptedBinding === null && (directProviderTurn || sessionOnlyObservation)) {
|
|
104
|
+
const observedSession = directProviderTurn ? executionSession : session;
|
|
105
|
+
if (observedSession === undefined
|
|
106
|
+
|| observedSession.adapterId !== adapterId
|
|
107
|
+
|| (!directProviderTurn && observedSession.runtimeGenerationId !== runtimeGenerationId)
|
|
108
|
+
|| observedSession.nativeSessionId !== nativeSessionId
|
|
109
|
+
|| observedSession.effective.workspace.root !== workspace) {
|
|
73
110
|
throw new Error("Runtime observation Hook Session does not match durable state.");
|
|
74
111
|
}
|
|
75
112
|
return {
|
|
@@ -82,35 +119,19 @@ export function resolveRuntimeHookTurnFence(environment, adapterId, payloadNativ
|
|
|
82
119
|
workspace
|
|
83
120
|
};
|
|
84
121
|
}
|
|
85
|
-
const activationReceiptId =
|
|
86
|
-
&& providerTurn !== undefined
|
|
87
|
-
&& managedProviderTurnId(providerTurn) === activeTurn?.id
|
|
122
|
+
const activationReceiptId = matchesProviderTurn
|
|
88
123
|
? providerTurn.attemptId
|
|
89
|
-
:
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
taskId,
|
|
94
|
-
roleName,
|
|
95
|
-
agentId,
|
|
96
|
-
nativeSessionId,
|
|
97
|
-
nativeTurnId: options.nativeTurnId
|
|
98
|
-
});
|
|
99
|
-
const acceptedBinding = acceptedTurn ?? (options.continuationId === undefined ? null : knownContinuationBinding(store.listEvents(taskId), {
|
|
100
|
-
taskId,
|
|
101
|
-
roleName,
|
|
102
|
-
agentId,
|
|
103
|
-
nativeSessionId,
|
|
104
|
-
continuationId: options.continuationId,
|
|
105
|
-
continuationGeneration: options.continuationGeneration ?? 1
|
|
106
|
-
}));
|
|
124
|
+
: providerTurn !== null
|
|
125
|
+
&& providerTurn !== undefined
|
|
126
|
+
&& managedProviderTurnId(providerTurn) === activeTurn?.id
|
|
127
|
+
? providerTurn.attemptId
|
|
128
|
+
: activeTurn === null ? undefined : formatTurnReceiptId(taskId, activeTurn.id);
|
|
107
129
|
const mailbox = lifecycleMailbox;
|
|
108
130
|
const exactReservation = isRuntimeLaunchReservation(mailbox?.processing, runtimeGenerationId)
|
|
109
131
|
&& !hasRuntimeCleanupObligation(mailbox);
|
|
110
|
-
const startupTurnId =
|
|
111
|
-
? undefined
|
|
112
|
-
: requireIdentity(durableTurnId, "Turn id");
|
|
132
|
+
const startupTurnId = reservedStartup ? activeTurn.id : undefined;
|
|
113
133
|
const startupReservation = startupTurnId !== undefined
|
|
134
|
+
&& reservedStartup
|
|
114
135
|
&& exactReservation
|
|
115
136
|
&& !hasRuntimeCleanupObligation(mailbox);
|
|
116
137
|
const startupTurn = startupTurnId === undefined
|
|
@@ -122,6 +143,12 @@ export function resolveRuntimeHookTurnFence(environment, adapterId, payloadNativ
|
|
|
122
143
|
&& startupReservation
|
|
123
144
|
&& startupTurn?.mode === "new"
|
|
124
145
|
&& session.status === "ended";
|
|
146
|
+
const resumedStartup = startupReservation
|
|
147
|
+
&& startupTurn?.mode === "resume"
|
|
148
|
+
&& session !== undefined
|
|
149
|
+
&& session.adapterId === adapterId
|
|
150
|
+
&& session.nativeSessionId === nativeSessionId
|
|
151
|
+
&& session.effective.workspace.root === workspace;
|
|
125
152
|
// The startup mode itself says whether Yui preallocated the native Session
|
|
126
153
|
// id or the Provider reports it. A preallocated startup is proven against
|
|
127
154
|
// Yui's deterministic runtime generation identity, which is stronger than comparing a
|
|
@@ -133,14 +160,17 @@ export function resolveRuntimeHookTurnFence(environment, adapterId, payloadNativ
|
|
|
133
160
|
const discoveredStartup = options.startupSession === "discovered"
|
|
134
161
|
&& (session === undefined || replacementStartup)
|
|
135
162
|
&& startupReservation;
|
|
136
|
-
const
|
|
137
|
-
?
|
|
163
|
+
const registeredTurnId = acceptedBinding === null && matchesProviderTurn
|
|
164
|
+
? managedProviderTurnId(providerTurn) ?? undefined
|
|
138
165
|
: undefined;
|
|
166
|
+
if (options.terminal === true && acceptedBinding === null && registeredTurnId === undefined) {
|
|
167
|
+
throw new Error("Runtime observation Hook terminal has no exact accepted execution binding.");
|
|
168
|
+
}
|
|
139
169
|
const terminalTurn = acceptedBinding !== null
|
|
140
170
|
? store.getTurn(taskId, acceptedBinding.fence.turnId)
|
|
141
|
-
:
|
|
171
|
+
: registeredTurnId === undefined
|
|
142
172
|
? null
|
|
143
|
-
: store.getTurn(taskId,
|
|
173
|
+
: store.getTurn(taskId, registeredTurnId);
|
|
144
174
|
const exactTerminal = terminalTurn !== null
|
|
145
175
|
&& terminalTurn.status !== "active"
|
|
146
176
|
&& terminalTurn.roleName === roleName
|
|
@@ -150,21 +180,24 @@ export function resolveRuntimeHookTurnFence(environment, adapterId, payloadNativ
|
|
|
150
180
|
if (activeTurn === null
|
|
151
181
|
&& !preallocatedStartup
|
|
152
182
|
&& !discoveredStartup
|
|
183
|
+
&& !resumedStartup
|
|
153
184
|
&& !exactTerminal
|
|
185
|
+
&& registeredTurnId === undefined
|
|
154
186
|
&& acceptedBinding === null) {
|
|
155
187
|
throw new Error("Runtime observation Hook has no matching durable in-flight Turn.");
|
|
156
188
|
}
|
|
157
189
|
const turnId = acceptedBinding?.fence.turnId
|
|
190
|
+
?? registeredTurnId
|
|
158
191
|
?? activeTurn?.id
|
|
159
|
-
?? startupTurnId
|
|
160
|
-
?? terminalTurnId;
|
|
192
|
+
?? startupTurnId;
|
|
161
193
|
const effectiveRuntimeGenerationId = acceptedBinding?.fence.runtimeGenerationId ?? runtimeGenerationId;
|
|
162
|
-
const turn = acceptedBinding !== null ||
|
|
194
|
+
const turn = acceptedBinding !== null || registeredTurnId !== undefined
|
|
163
195
|
? terminalTurn
|
|
164
196
|
: store.getActiveTurn(taskId, roleName);
|
|
165
197
|
if (turn === null
|
|
166
198
|
|| turn.id !== turnId
|
|
167
|
-
|| (acceptedBinding === null && !exactTerminal && turn.status !== "active")
|
|
199
|
+
|| (acceptedBinding === null && registeredTurnId === undefined && !exactTerminal && turn.status !== "active")
|
|
200
|
+
|| turn.roleName !== roleName
|
|
168
201
|
|| turn.effective.agentId !== agentId
|
|
169
202
|
|| turn.effective.adapterId !== adapterId) {
|
|
170
203
|
throw new Error("Runtime observation Hook Turn does not match durable active state.");
|
|
@@ -172,7 +205,7 @@ export function resolveRuntimeHookTurnFence(environment, adapterId, payloadNativ
|
|
|
172
205
|
if (turn.effective.workspace.root !== workspace) {
|
|
173
206
|
throw new Error("Runtime observation Hook workspace does not match the durable Turn snapshot.");
|
|
174
207
|
}
|
|
175
|
-
if (session !== undefined && acceptedBinding === null && !replacementStartup) {
|
|
208
|
+
if (session !== undefined && acceptedBinding === null && !matchesProviderTurn && !replacementStartup && !resumedStartup) {
|
|
176
209
|
if (session.adapterId !== adapterId
|
|
177
210
|
|| session.runtimeGenerationId !== effectiveRuntimeGenerationId
|
|
178
211
|
|| session.nativeSessionId !== nativeSessionId
|
|
@@ -180,7 +213,7 @@ export function resolveRuntimeHookTurnFence(environment, adapterId, payloadNativ
|
|
|
180
213
|
throw new Error("Runtime observation Hook Session does not match its durable generation.");
|
|
181
214
|
}
|
|
182
215
|
}
|
|
183
|
-
else if (acceptedBinding === null && (session === undefined || replacementStartup)) {
|
|
216
|
+
else if (acceptedBinding === null && !matchesProviderTurn && (session === undefined || replacementStartup)) {
|
|
184
217
|
if (!discoveredStartup && !preallocatedStartup) {
|
|
185
218
|
throw new Error("Runtime observation Hook launch is not durably reserved.");
|
|
186
219
|
}
|
|
@@ -225,15 +258,18 @@ function knownContinuationBinding(events, expected) {
|
|
|
225
258
|
return binding;
|
|
226
259
|
}
|
|
227
260
|
function acceptedTurnBinding(events, expected) {
|
|
261
|
+
if (expected.nativeTurnId === undefined && expected.attemptId === undefined)
|
|
262
|
+
return null;
|
|
228
263
|
const matches = events
|
|
229
264
|
.map(runtimeObservationFromTaskEvent)
|
|
230
265
|
.filter((observation) => observation !== null
|
|
231
|
-
&&
|
|
266
|
+
&& ["turn.accepted", "turn.completed", "turn.failed", "turn.cancelled"].includes(observation.kind)
|
|
232
267
|
&& observation.fence.taskId === expected.taskId
|
|
233
268
|
&& observation.fence.roleName === expected.roleName
|
|
234
269
|
&& observation.fence.agentId === expected.agentId
|
|
235
270
|
&& observation.fence.nativeSessionId === expected.nativeSessionId
|
|
236
|
-
&& observation.fence.
|
|
271
|
+
&& ((expected.attemptId !== undefined && observation.fence.receiptId === expected.attemptId)
|
|
272
|
+
|| (expected.nativeTurnId !== undefined && observation.fence.nativeTurnId === expected.nativeTurnId))
|
|
237
273
|
&& observation.fence.turnId !== undefined)
|
|
238
274
|
.sort((left, right) => (left.receivedAt.localeCompare(right.receivedAt)
|
|
239
275
|
|| (left.sequence ?? -1) - (right.sequence ?? -1)
|
|
@@ -242,9 +278,12 @@ function acceptedTurnBinding(events, expected) {
|
|
|
242
278
|
const binding = matches.at(-1) ?? null;
|
|
243
279
|
if (binding === null)
|
|
244
280
|
return null;
|
|
245
|
-
if (matches.some((candidate) => candidate.fence.
|
|
281
|
+
if (matches.some((candidate) => ((expected.attemptId !== undefined && candidate.fence.receiptId !== expected.attemptId)
|
|
282
|
+
|| (expected.nativeTurnId !== undefined && candidate.fence.nativeTurnId !== undefined
|
|
283
|
+
&& candidate.fence.nativeTurnId !== expected.nativeTurnId)
|
|
284
|
+
|| candidate.fence.turnId !== binding.fence.turnId
|
|
246
285
|
|| candidate.fence.runtimeGenerationId !== binding.fence.runtimeGenerationId
|
|
247
|
-
|| candidate.fence.receiptId !== binding.fence.receiptId)) {
|
|
286
|
+
|| candidate.fence.receiptId !== binding.fence.receiptId))) {
|
|
248
287
|
throw new Error("Runtime observation Hook native Turn has conflicting durable Turn bindings.");
|
|
249
288
|
}
|
|
250
289
|
return binding;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
3
|
-
import { createRuntimeBinding, RuntimeGenerationMismatchError, RuntimeHostContentionError, RuntimeLaunchError } from "../runtime/index.js";
|
|
3
|
+
import { createRuntimeBinding, RuntimeGenerationMismatchError, RuntimeHostContentionError, RuntimeHostUnavailableError, RuntimeLaunchError } from "../runtime/index.js";
|
|
4
4
|
import { sameEffectiveLaunch, validateEffectiveLaunchSnapshot } from "../executor/effectiveLaunch.js";
|
|
5
5
|
class RuntimeBindingContractError extends Error {
|
|
6
6
|
constructor(message, options) {
|
|
@@ -76,8 +76,9 @@ export class RuntimeLaunchCoordinator {
|
|
|
76
76
|
if (request.owner.scope === "global" && request.managedWorkspace !== undefined) {
|
|
77
77
|
throw new Error("A global runtime cannot use a Task ManagedWorkspace.");
|
|
78
78
|
}
|
|
79
|
+
let currentRequest = request;
|
|
79
80
|
const assertLaunchCurrent = () => {
|
|
80
|
-
this.#assertCurrent?.(
|
|
81
|
+
this.#assertCurrent?.(currentRequest);
|
|
81
82
|
assertCurrent?.();
|
|
82
83
|
};
|
|
83
84
|
const proposedGenerationId = requireText(this.#createGenerationId(), "Launch generation id");
|
|
@@ -155,8 +156,20 @@ export class RuntimeLaunchCoordinator {
|
|
|
155
156
|
throw new Error("Runtime host reported its pre-start launch fence more than once.");
|
|
156
157
|
}
|
|
157
158
|
validateRuntimeLaunchPreflight(preflight, request, runtimeGenerationId);
|
|
159
|
+
this.reservations.confirmRuntimeLaunchReservation({
|
|
160
|
+
owner: request.owner,
|
|
161
|
+
runtimeGenerationId
|
|
162
|
+
}, assertLaunchCurrent);
|
|
158
163
|
preflightObserved = true;
|
|
159
164
|
beforeHostStart?.(preflight);
|
|
165
|
+
// The pre-start persistence callback hands the fixed native Session to
|
|
166
|
+
// this reserved activation. From here on, fence against that exact new
|
|
167
|
+
// identity, not the historical Host we originally set out to restore.
|
|
168
|
+
// Do not accept both identities: a later change back is stale as well.
|
|
169
|
+
if (request.mode === "resume" && request.hostActivationId !== undefined) {
|
|
170
|
+
currentRequest = { ...request, hostActivationId: runtimeGenerationId };
|
|
171
|
+
}
|
|
172
|
+
assertLaunchCurrent();
|
|
160
173
|
};
|
|
161
174
|
let binding;
|
|
162
175
|
try {
|
|
@@ -206,15 +219,21 @@ export class RuntimeLaunchCoordinator {
|
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
221
|
catch (error) {
|
|
222
|
+
if (error instanceof RuntimeHostUnavailableError) {
|
|
223
|
+
// A failed reused attachment provides no authority to detach its
|
|
224
|
+
// native Conversation or schedule owner-wide physical cleanup.
|
|
225
|
+
throw error;
|
|
226
|
+
}
|
|
209
227
|
if (error instanceof RuntimeGenerationMismatchError) {
|
|
210
|
-
|
|
211
|
-
|
|
228
|
+
// The observed Host is not the requested activation. Neither killing
|
|
229
|
+
// it nor scheduling owner-wide cleanup is justified by that conflict.
|
|
230
|
+
throw new RuntimeLaunchError(false, runtimeGenerationId, error.message, "generation-mismatch", { cause: error });
|
|
212
231
|
}
|
|
213
232
|
if (error instanceof RuntimeHostContentionError && reusedConfirmedRunningHost) {
|
|
214
233
|
// The exact recovered generation remains authoritative. A late human
|
|
215
234
|
// writer is transient backpressure and must not settle, clean, or
|
|
216
235
|
// terminalize that reservation.
|
|
217
|
-
throw new RuntimeLaunchError(true, runtimeGenerationId, error.message, error.reason);
|
|
236
|
+
throw new RuntimeLaunchError(true, runtimeGenerationId, error.message, error.reason, { cause: error });
|
|
218
237
|
}
|
|
219
238
|
if (error instanceof RuntimeHostContentionError
|
|
220
239
|
&& !reusedConfirmedRunningHost) {
|
|
@@ -239,7 +258,7 @@ export class RuntimeLaunchCoordinator {
|
|
|
239
258
|
}
|
|
240
259
|
throw new RuntimeLaunchError(true, runtimeGenerationId, error.message, error instanceof RuntimeHostContentionError
|
|
241
260
|
? error.reason
|
|
242
|
-
: "previous-process");
|
|
261
|
+
: "previous-process", { cause: error });
|
|
243
262
|
}
|
|
244
263
|
if (error instanceof RuntimeBindingContractError) {
|
|
245
264
|
// Never pass an untrusted hostRef to stop(). Reconcile the requested
|
|
@@ -251,7 +270,7 @@ export class RuntimeLaunchCoordinator {
|
|
|
251
270
|
throw error;
|
|
252
271
|
}
|
|
253
272
|
if (reusedConfirmedRunningHost && binding.hostCreated === true) {
|
|
254
|
-
await this.#compensateStartedHost(request.owner, binding, runtimeGenerationId, runtimeIsolation, new Error(`Runtime host was recreated while recovering an existing generation: ${request.owner.roleName}.`));
|
|
273
|
+
await this.#compensateStartedHost(request.owner, binding, runtimeGenerationId, runtimeIsolation, reusedConfirmedRunningHost, new Error(`Runtime host was recreated while recovering an existing generation: ${request.owner.roleName}.`));
|
|
255
274
|
}
|
|
256
275
|
try {
|
|
257
276
|
assertLaunchCurrent();
|
|
@@ -276,11 +295,11 @@ export class RuntimeLaunchCoordinator {
|
|
|
276
295
|
}
|
|
277
296
|
}
|
|
278
297
|
catch (error) {
|
|
279
|
-
await this.#compensateStartedHost(request.owner, binding, runtimeGenerationId, runtimeIsolation, error);
|
|
298
|
+
await this.#compensateStartedHost(request.owner, binding, runtimeGenerationId, runtimeIsolation, reusedConfirmedRunningHost, error);
|
|
280
299
|
}
|
|
281
300
|
return binding;
|
|
282
301
|
}
|
|
283
|
-
async #settleFailedStart(request, runtimeGenerationId, reusedConfirmedRunningHost, runtimeIsolation
|
|
302
|
+
async #settleFailedStart(request, runtimeGenerationId, reusedConfirmedRunningHost, runtimeIsolation) {
|
|
284
303
|
let inspection;
|
|
285
304
|
try {
|
|
286
305
|
inspection = await this.host.inspectOwner(request.owner);
|
|
@@ -288,7 +307,7 @@ export class RuntimeLaunchCoordinator {
|
|
|
288
307
|
catch {
|
|
289
308
|
// A rebind/preflight failure did not create the already-confirmed host.
|
|
290
309
|
// Preserve its generation when the follow-up probe is merely unknown.
|
|
291
|
-
if (!reusedConfirmedRunningHost
|
|
310
|
+
if (!reusedConfirmedRunningHost)
|
|
292
311
|
this.#requireCleanup(request.owner);
|
|
293
312
|
return;
|
|
294
313
|
}
|
|
@@ -307,11 +326,18 @@ export class RuntimeLaunchCoordinator {
|
|
|
307
326
|
}
|
|
308
327
|
return;
|
|
309
328
|
}
|
|
310
|
-
if (reusedConfirmedRunningHost
|
|
329
|
+
if (reusedConfirmedRunningHost)
|
|
311
330
|
return;
|
|
312
331
|
this.#requireCleanup(request.owner);
|
|
313
332
|
}
|
|
314
|
-
async #compensateStartedHost(owner, binding, runtimeGenerationId, runtimeIsolation, cause) {
|
|
333
|
+
async #compensateStartedHost(owner, binding, runtimeGenerationId, runtimeIsolation, reusedConfirmedRunningHost, cause) {
|
|
334
|
+
if (reusedConfirmedRunningHost && binding.hostCreated !== true) {
|
|
335
|
+
// Reattaching an existing activation gives this launch no ownership of
|
|
336
|
+
// it. A fresh activation inside a persistent Host is different: this
|
|
337
|
+
// launch still owns its startup and isolation cleanup even when the
|
|
338
|
+
// physical Host did not need to be created.
|
|
339
|
+
throw new RuntimeLaunchStateChangedError(`Runtime launch state changed while reusing a Host: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
340
|
+
}
|
|
315
341
|
try {
|
|
316
342
|
await this.host.stop(binding);
|
|
317
343
|
}
|
|
@@ -18,6 +18,8 @@ export async function runRuntimeObservationHookCommand(stdinJson, environment =
|
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
const parsed = parseRuntimeObservationHook(stdinJson, environment, now, dependencies);
|
|
21
|
+
if (parsed.observations.length === 0)
|
|
22
|
+
return;
|
|
21
23
|
const inbox = new FileRuntimeEventInbox(parsed.home);
|
|
22
24
|
for (const observation of parsed.observations)
|
|
23
25
|
inbox.enqueueObservation(observation);
|
|
@@ -84,6 +86,20 @@ export function parseRuntimeObservationHook(stdinJson, environment, now = new Da
|
|
|
84
86
|
const driverId = requireIdentity(environment.YUI_DRIVER_ID, "Agent Driver id");
|
|
85
87
|
const driver = drivers.require(driverId);
|
|
86
88
|
const hookEventName = requireIdentity(payload.hook_event_name, "Agent Driver Hook event name");
|
|
89
|
+
if (environment.YUI_SESSION_SCOPE === "task" && driver.adapterId === "claude"
|
|
90
|
+
&& environment.YUI_ADAPTER_ID === "claude") {
|
|
91
|
+
// Existing native Conversations may retain an old Yui observer plugin.
|
|
92
|
+
// Only the managed stream owns execution facts: Hooks do not expose its
|
|
93
|
+
// input attempt, and associating them with the currently active Turn can
|
|
94
|
+
// steal a successor's result. This observer is not a permission handler;
|
|
95
|
+
// Claude's configured tool policy and other user plugins remain intact.
|
|
96
|
+
return {
|
|
97
|
+
home: requireIdentity(environment.YUI_HOME, "YUI_HOME"),
|
|
98
|
+
taskId: requireIdentity(environment.YUI_TASK_ID, "Task id"),
|
|
99
|
+
roleName: requireIdentity(environment.YUI_ROLE, "Role name"),
|
|
100
|
+
observations: Object.freeze([])
|
|
101
|
+
};
|
|
102
|
+
}
|
|
87
103
|
const receivedAt = now.toISOString();
|
|
88
104
|
const sequence = (dependencies.sequence ?? monotonicSequence)();
|
|
89
105
|
const occurrenceId = `${receivedAt}:${sequence}`;
|
|
@@ -124,7 +140,7 @@ export function parseRuntimeObservationHook(stdinJson, environment, now = new Da
|
|
|
124
140
|
conversationId: fence.nativeSessionId,
|
|
125
141
|
activationId: fence.runtimeGenerationId,
|
|
126
142
|
nativeSessionId: fence.nativeSessionId,
|
|
127
|
-
nativeTurnId: nativeTurnId
|
|
143
|
+
...(nativeTurnId === undefined ? {} : { nativeTurnId }),
|
|
128
144
|
receiptId: fence.receiptId ?? formatTurnReceiptId(fence.taskId, fence.turnId)
|
|
129
145
|
},
|
|
130
146
|
payload
|
|
@@ -8,6 +8,7 @@ import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
|
|
|
8
8
|
import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
9
9
|
import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
|
|
10
10
|
import { resolveRuntimeHookTurnFence } from "./runtimeHookTurnFence.js";
|
|
11
|
+
import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
11
12
|
let structuredSequence = 0;
|
|
12
13
|
export async function publishStructuredProviderStarted(input) {
|
|
13
14
|
if (input.started.clientOwned)
|
|
@@ -58,39 +59,25 @@ export async function publishStructuredProviderAccepted(input) {
|
|
|
58
59
|
driverId: driver.id,
|
|
59
60
|
runtimeGenerationId: fence.runtimeGenerationId,
|
|
60
61
|
conversationId: input.receipt.conversationId,
|
|
61
|
-
activationId:
|
|
62
|
+
activationId: executionActivationId(input.home, fence, input.activationId, input.receipt.nativeSessionId),
|
|
62
63
|
nativeSessionId: input.receipt.nativeSessionId,
|
|
63
|
-
|
|
64
|
+
...(input.receipt.nativeTurnId === undefined ? {} : {
|
|
65
|
+
nativeTurnId: input.receipt.nativeTurnId
|
|
66
|
+
}),
|
|
64
67
|
// The structured Host owns the exact input attempt identity. Runtime
|
|
65
68
|
// descriptor receipts name the Turn bootstrap and must not overwrite a
|
|
66
69
|
// continuation or human-takeover Turn.
|
|
67
70
|
receiptId: input.receipt.attemptId
|
|
68
71
|
};
|
|
69
72
|
const baseSequence = nextStructuredSequence();
|
|
73
|
+
// Opening owns the Session/Activation lifecycle. A receipt arriving after
|
|
74
|
+
// its terminal must not reopen either lifecycle as an acceptance side effect.
|
|
70
75
|
const observations = [observation({
|
|
71
|
-
kind: startupSession === "preallocated" ? "session.ready" : "session.started",
|
|
72
|
-
observedAt,
|
|
73
|
-
sequence: baseSequence,
|
|
74
|
-
ordinal: 0,
|
|
75
|
-
fence: commonFence
|
|
76
|
-
}), observation({
|
|
77
|
-
kind: "conversation.observed",
|
|
78
|
-
observedAt,
|
|
79
|
-
sequence: baseSequence,
|
|
80
|
-
ordinal: 1,
|
|
81
|
-
fence: commonFence,
|
|
82
|
-
payload: { recoverability: "recoverable" }
|
|
83
|
-
}), observation({
|
|
84
|
-
kind: "activation.started",
|
|
85
|
-
observedAt,
|
|
86
|
-
sequence: baseSequence,
|
|
87
|
-
ordinal: 2,
|
|
88
|
-
fence: commonFence
|
|
89
|
-
}), observation({
|
|
90
76
|
kind: "turn.accepted",
|
|
77
|
+
authority: input.receipt.acceptance === "transport" ? "transport" : "provider-structured",
|
|
91
78
|
observedAt,
|
|
92
79
|
sequence: baseSequence,
|
|
93
|
-
ordinal:
|
|
80
|
+
ordinal: 0,
|
|
94
81
|
fence: commonFence
|
|
95
82
|
})];
|
|
96
83
|
await persistAndApply(input.home, observations, fence.taskId, fence.roleName);
|
|
@@ -215,6 +202,7 @@ export async function publishStructuredProviderTerminal(input) {
|
|
|
215
202
|
const fence = resolveRuntimeHookTurnFence(input.environment, adapterId, input.terminal.nativeSessionId, {
|
|
216
203
|
terminal: true,
|
|
217
204
|
nativeTurnId: input.terminal.nativeTurnId,
|
|
205
|
+
attemptId: input.terminal.attemptId,
|
|
218
206
|
...(input.terminal.clientOwned ? {} : { sessionOnly: true })
|
|
219
207
|
});
|
|
220
208
|
const kind = input.terminal.status === "completed"
|
|
@@ -226,7 +214,9 @@ export async function publishStructuredProviderTerminal(input) {
|
|
|
226
214
|
...(input.terminal.input === undefined ? {} : { input: input.terminal.input }),
|
|
227
215
|
...(transported.status === "completed"
|
|
228
216
|
? { output: transported.output }
|
|
229
|
-
:
|
|
217
|
+
: transported.failureReason === "runtime-failed"
|
|
218
|
+
? { resultTransportDiagnostic: transported.diagnostic }
|
|
219
|
+
: {})
|
|
230
220
|
}
|
|
231
221
|
: kind === "turn.failed"
|
|
232
222
|
? {
|
|
@@ -264,10 +254,14 @@ export async function publishStructuredProviderTerminal(input) {
|
|
|
264
254
|
driverId: driver.id,
|
|
265
255
|
runtimeGenerationId: fence.runtimeGenerationId,
|
|
266
256
|
conversationId: input.terminal.conversationId,
|
|
267
|
-
activationId:
|
|
257
|
+
activationId: executionActivationId(input.home, fence, input.activationId, input.terminal.nativeSessionId),
|
|
268
258
|
nativeSessionId: input.terminal.nativeSessionId,
|
|
269
|
-
|
|
270
|
-
|
|
259
|
+
...(input.terminal.nativeTurnId === undefined ? {} : {
|
|
260
|
+
nativeTurnId: input.terminal.nativeTurnId
|
|
261
|
+
}),
|
|
262
|
+
...(input.terminal.attemptId === undefined
|
|
263
|
+
? fence.receiptId === undefined ? {} : { receiptId: fence.receiptId }
|
|
264
|
+
: { receiptId: input.terminal.attemptId })
|
|
271
265
|
},
|
|
272
266
|
payload
|
|
273
267
|
});
|
|
@@ -312,7 +306,7 @@ function observation(input) {
|
|
|
312
306
|
eventId,
|
|
313
307
|
semanticKey: runtimeObservationSemanticKey(partial),
|
|
314
308
|
kind: input.kind,
|
|
315
|
-
authority: "provider-structured",
|
|
309
|
+
authority: input.authority ?? "provider-structured",
|
|
316
310
|
receivedAt: new Date().toISOString(),
|
|
317
311
|
observedAt: input.observedAt,
|
|
318
312
|
sequence: input.sequence,
|
|
@@ -336,6 +330,24 @@ function requireIdentity(value, label) {
|
|
|
336
330
|
}
|
|
337
331
|
return value.trim();
|
|
338
332
|
}
|
|
333
|
+
function executionActivationId(home, fence, sourceActivationId, nativeSessionId) {
|
|
334
|
+
const sourceId = requireIdentity(sourceActivationId, "Provider Activation id");
|
|
335
|
+
const store = openCurrentTaskStore(home);
|
|
336
|
+
try {
|
|
337
|
+
const binding = store.getTaskRoleSessionSet(fence.taskId, fence.roleName)?.providerBinding;
|
|
338
|
+
const source = binding?.activations.find(entry => entry.activationId === sourceId);
|
|
339
|
+
const original = binding?.activations.find(entry => entry.activationId === fence.runtimeGenerationId);
|
|
340
|
+
if (source?.conversationId !== nativeSessionId || original?.conversationId !== nativeSessionId) {
|
|
341
|
+
throw new Error("Structured execution observation source does not belong to its native Conversation.");
|
|
342
|
+
}
|
|
343
|
+
// A reattached collector can observe the old execution, but its current
|
|
344
|
+
// attachment must not replace that execution's durable Activation identity.
|
|
345
|
+
return original.activationId;
|
|
346
|
+
}
|
|
347
|
+
finally {
|
|
348
|
+
store.close();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
339
351
|
async function persistAndApply(home, observations, taskId, roleName) {
|
|
340
352
|
const inbox = new FileRuntimeEventInbox(home);
|
|
341
353
|
for (const entry of observations)
|
|
@@ -20,6 +20,11 @@ export function controllerCallMayHaveApplied(error) {
|
|
|
20
20
|
&& [
|
|
21
21
|
"CONTROLLER_TIMEOUT",
|
|
22
22
|
"CONTROLLER_UNAVAILABLE",
|
|
23
|
+
// Raised only after the request write began, so the Controller may well
|
|
24
|
+
// have committed it. Omitting it reported a genuinely unknown outcome as
|
|
25
|
+
// a definite failure, which is the one classification callers must not
|
|
26
|
+
// make: it invites treating possibly-applied work as never applied.
|
|
27
|
+
"CONTROLLER_DELIVERY_UNKNOWN",
|
|
23
28
|
"INVALID_RESPONSE"
|
|
24
29
|
].includes(error.code);
|
|
25
30
|
}
|