@zq-silk/yui 0.12.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -9
- package/dist/cli/commandCatalog.js +11 -6
- package/dist/cli/interactionPolicy.js +5 -6
- package/dist/cli/updateCommand.js +3 -1
- package/dist/cli/updateOrchestrator.js +173 -28
- package/dist/cli/updatePorts.js +137 -8
- package/dist/cli/upgradeCommand.js +19 -9
- package/dist/cli.js +51 -14
- package/dist/commands/configCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +2 -1
- package/dist/commands/taskCommands.js +80 -29
- package/dist/commands/taskContextCommand.js +6 -2
- package/dist/commands/taskRoleRuntimeStatus.js +31 -7
- package/dist/config/configCatalog.js +1 -1
- package/dist/controller/clientRuntime.js +38 -2
- package/dist/controller/controller.js +23 -15
- package/dist/controller/fileSchedulerStoreAdapter.js +248 -138
- package/dist/controller/runtime.js +56 -1
- package/dist/controller/runtimeHookRunFence.js +19 -4
- package/dist/controller/structuredProviderObservation.js +20 -3
- package/dist/core/controllerClient.js +20 -2
- package/dist/core/controllerServer.js +1 -0
- package/dist/executor/agentExecutor.js +48 -46
- package/dist/executor/fileRoleLaunchPlanner.js +94 -30
- package/dist/lifecycle/exactRunTerminalization.js +68 -3
- package/dist/observability/executionAudit.js +5 -0
- package/dist/release/runtimeRelease.js +20 -0
- package/dist/run/recoveryProjection.js +45 -6
- package/dist/runtime/agentHost.js +159 -85
- package/dist/runtime/conversationSwitch.js +277 -0
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/launchBroker.js +12 -0
- package/dist/runtime/processExitOutbox.js +88 -0
- package/dist/runtime/providerRuntimeIdentity.js +29 -1
- package/dist/runtime/runtimeHealthPolicy.js +5 -5
- package/dist/runtime/runtimeObservation.js +15 -0
- package/dist/runtime/runtimeProjection.js +6 -7
- package/dist/runtime/tmuxAdapters.js +4 -1
- package/dist/scheduler/activeRoleRunDelivery.js +31 -196
- package/dist/scheduler/leaderWakeupProcessor.js +39 -133
- package/dist/scheduler/roleRunStall.js +53 -17
- package/dist/storage/sqliteSchema.js +62 -26
- package/dist/storage/sqliteStore.js +23 -4
- package/dist/storage/upgrade/homeClassification.js +52 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
- package/dist/task/nextAction.js +0 -34
- package/dist/web/webSnapshot.js +3 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +7 -4
- package/skills/yui-operator/SKILL.md +4 -4
- package/skills/yui-reviewer/SKILL.md +7 -4
- package/dist/lifecycle/taskRoleSessionReset.js +0 -118
|
@@ -36,6 +36,7 @@ import { createRuntimeResourceActivityTracker } from "./resourceInventory.js";
|
|
|
36
36
|
import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
|
|
37
37
|
import { launchBrokerForHome } from "../runtime/launchBroker.js";
|
|
38
38
|
import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
|
|
39
|
+
import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
|
|
39
40
|
import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExitStore.js";
|
|
40
41
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
41
42
|
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
@@ -385,6 +386,12 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
385
386
|
environment: options.environment
|
|
386
387
|
});
|
|
387
388
|
const lifecycleDispatcher = createRuntimeLifecycleDispatcher(store, schedulerStore, sessionHost, options.dispatcher, signalRuntimeCleanup, launchCoordinator, planner);
|
|
389
|
+
// Process-exit observations are persisted by the Agent Host before socket
|
|
390
|
+
// delivery. Drain them while this Controller is still the only storage
|
|
391
|
+
// writer and before it begins accepting new work after a handover.
|
|
392
|
+
await replayRuntimeProcessExitOutbox(home, async (observation) => {
|
|
393
|
+
await lifecycleDispatcher("runtime.process-exit-observe", observation);
|
|
394
|
+
});
|
|
388
395
|
// f7/rr5: This same inbox feeds the supervisor's terminal channel and the
|
|
389
396
|
// runtime event processor. When a Job reaches a terminal state, the
|
|
390
397
|
// supervisor enqueues a durable-job-terminal event; the processor drains it
|
|
@@ -577,6 +584,11 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
577
584
|
}
|
|
578
585
|
return { recorded: true };
|
|
579
586
|
}
|
|
587
|
+
if (method === "runtime.conversation-switch-detach") {
|
|
588
|
+
const value = conversationSwitchDetachParams(params);
|
|
589
|
+
const outcome = schedulerStore.detachAgentHostProviderForConversationSwitch(value);
|
|
590
|
+
return { recorded: outcome === "detached", outcome };
|
|
591
|
+
}
|
|
580
592
|
if (method === "runtime.provider-turn-submission-resolve") {
|
|
581
593
|
const value = providerTurnControlParams(params);
|
|
582
594
|
const status = params.status;
|
|
@@ -671,6 +683,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
671
683
|
|| !Number.isSafeInteger(hostPid) || hostPid <= 0) {
|
|
672
684
|
throw applicationError("INVALID_PARAMS", "Launch redemption identity is invalid.");
|
|
673
685
|
}
|
|
686
|
+
// The launch payload is validated at reservation time and every member
|
|
687
|
+
// of its discriminated Provider-control union is JSON serializable.
|
|
674
688
|
return launchBrokerForHome(store.rootDirectory()).redeem(launchId, ticket);
|
|
675
689
|
}
|
|
676
690
|
if (method === "runtime.replace-agent-environment") {
|
|
@@ -735,6 +749,9 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
735
749
|
const activeRun = request.scope === "task"
|
|
736
750
|
? store.getActiveAgentRun(request.taskId, request.roleName)
|
|
737
751
|
: null;
|
|
752
|
+
if (request.scope === "task" && activeRun === null) {
|
|
753
|
+
throw applicationError("INVALID_PARAMS", "Task Role runtime attachment requires an admitted active Run; it cannot create an empty Provider Conversation.");
|
|
754
|
+
}
|
|
738
755
|
const managedWorkspace = request.scope === "task"
|
|
739
756
|
? activeRun?.workspace
|
|
740
757
|
?? currentDesiredManagedWorkspace(store, request.taskId, request.roleName)
|
|
@@ -754,8 +771,21 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
754
771
|
throw applicationError("INVALID_PARAMS", `Configured Agent does not match Role: ${effective.agentId}.`);
|
|
755
772
|
}
|
|
756
773
|
validateLifecycleEnvironment(request.environment, agent);
|
|
757
|
-
const mode = roleAgentSessionResumeMode(sessions, effective.agentId, effective);
|
|
758
774
|
const session = sessions?.sessions[effective.agentId];
|
|
775
|
+
const managedTurnDispatch = activeRun !== null && (activeRun.pushedAt === undefined
|
|
776
|
+
|| activeRun.providerRetry?.state === "dispatching"
|
|
777
|
+
|| activeRun.controlRequest?.state === "dispatching");
|
|
778
|
+
// An ensure call may reattach the already-admitted Run to its existing
|
|
779
|
+
// Conversation, but it may not manufacture a fresh Conversation without a
|
|
780
|
+
// pending managed Turn. Fresh replacement remains owned by Run dispatch.
|
|
781
|
+
const mode = activeRun === null
|
|
782
|
+
? roleAgentSessionResumeMode(sessions, effective.agentId, effective)
|
|
783
|
+
: managedTurnDispatch ? activeRun.mode : "resume";
|
|
784
|
+
if (request.scope === "task" && mode === "resume"
|
|
785
|
+
&& (session?.nativeSessionId === undefined
|
|
786
|
+
|| session.nativeSessionId.trim().length === 0)) {
|
|
787
|
+
throw applicationError("INVALID_PARAMS", "Task Role runtime attachment cannot resume because its Provider Conversation identity is missing.");
|
|
788
|
+
}
|
|
759
789
|
const owner = request.scope === "task"
|
|
760
790
|
? { scope: "task", taskId: request.taskId, roleName: request.roleName }
|
|
761
791
|
: { scope: "global", roleName: request.roleName };
|
|
@@ -1105,6 +1135,31 @@ function providerTurnControlParams(params) {
|
|
|
1105
1135
|
now: new Date(observedAt)
|
|
1106
1136
|
};
|
|
1107
1137
|
}
|
|
1138
|
+
function conversationSwitchDetachParams(params) {
|
|
1139
|
+
if (typeof params !== "object" || params === null || Array.isArray(params)) {
|
|
1140
|
+
throw applicationError("INVALID_PARAMS", "Provider Conversation detachment params are invalid.");
|
|
1141
|
+
}
|
|
1142
|
+
const value = params;
|
|
1143
|
+
const nextAuthorityEpoch = value.nextAuthorityEpoch;
|
|
1144
|
+
const observedAt = requiredParam(value.observedAt);
|
|
1145
|
+
if (!Number.isSafeInteger(nextAuthorityEpoch) || nextAuthorityEpoch < 1
|
|
1146
|
+
|| !Number.isFinite(Date.parse(observedAt))) {
|
|
1147
|
+
throw applicationError("INVALID_PARAMS", "Provider Conversation detachment fence is invalid.");
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
taskId: requiredParam(value.taskId),
|
|
1151
|
+
roleName: requiredParam(value.roleName),
|
|
1152
|
+
runId: requiredParam(value.runId),
|
|
1153
|
+
agentId: requiredParam(value.agentId),
|
|
1154
|
+
launchId: requiredParam(value.launchId),
|
|
1155
|
+
previousConversationId: requiredParam(value.previousConversationId),
|
|
1156
|
+
previousNativeSessionId: requiredParam(value.previousNativeSessionId),
|
|
1157
|
+
previousActivationId: requiredParam(value.previousActivationId),
|
|
1158
|
+
nextAuthorityEpoch: nextAuthorityEpoch,
|
|
1159
|
+
nextAuthorityHolderId: requiredParam(value.nextAuthorityHolderId),
|
|
1160
|
+
now: new Date(observedAt)
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1108
1163
|
function abortableDelay(milliseconds, signal) {
|
|
1109
1164
|
return new Promise((resolve, reject) => {
|
|
1110
1165
|
const timer = setTimeout(() => {
|
|
@@ -4,6 +4,7 @@ import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.j
|
|
|
4
4
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
5
5
|
import { YUI_CONTROL_PLANE_DESCRIPTOR, YUI_TASK_RUNTIME_DESCRIPTOR, assertExactTaskRuntimeEnvironment, exactControlPlaneDigest, parseExactControlPlaneDescriptor, refreshReusedTaskRuntimeDescriptorSource } from "../runtime/exactControlPlane.js";
|
|
6
6
|
import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
|
|
7
|
+
import { conversationReplacementBasis } from "../runtime/conversationSwitch.js";
|
|
7
8
|
/**
|
|
8
9
|
* Resolves turn identity from the current durable in-flight fence. The one
|
|
9
10
|
* exception is a Driver-declared startup Session Hook, which can arrive before
|
|
@@ -101,14 +102,27 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
|
|
|
101
102
|
const startupRun = startupRunId === undefined
|
|
102
103
|
? null
|
|
103
104
|
: store.getAgentRun(taskId, startupRunId);
|
|
105
|
+
const replacementStartup = options.startupSession !== undefined
|
|
106
|
+
&& session !== undefined
|
|
107
|
+
&& sessions !== null
|
|
108
|
+
&& startupReservation
|
|
109
|
+
&& startupRun?.mode === "new"
|
|
110
|
+
&& conversationReplacementBasis({
|
|
111
|
+
sessions,
|
|
112
|
+
events: store.listEvents(taskId),
|
|
113
|
+
mailbox: roleMailbox,
|
|
114
|
+
roleName,
|
|
115
|
+
runId: startupRun.id,
|
|
116
|
+
runMode: startupRun.mode
|
|
117
|
+
}) !== null;
|
|
104
118
|
const preallocatedStartup = options.startupSession === "preallocated"
|
|
105
119
|
&& expectedNativeSessionId !== undefined
|
|
106
|
-
&& session === undefined
|
|
120
|
+
&& (session === undefined || replacementStartup)
|
|
107
121
|
&& startupReservation
|
|
108
122
|
&& nativeSessionId === nativeSessionIdForLaunch(home, launchId, agentId, adapterId);
|
|
109
123
|
const discoveredStartup = options.startupSession === "discovered"
|
|
110
124
|
&& expectedNativeSessionId === undefined
|
|
111
|
-
&& session === undefined
|
|
125
|
+
&& (session === undefined || replacementStartup)
|
|
112
126
|
&& startupReservation;
|
|
113
127
|
const terminalRunId = options.terminal === true && acceptedBinding === null
|
|
114
128
|
? requireIdentity(environment.YUI_RUN_ID ?? runtime?.runId, "Run id")
|
|
@@ -154,6 +168,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
|
|
|
154
168
|
if (acceptedBinding === null
|
|
155
169
|
&& runtime !== undefined
|
|
156
170
|
&& session !== undefined
|
|
171
|
+
&& !replacementStartup
|
|
157
172
|
&& sessionLaunchId !== undefined
|
|
158
173
|
&& typeof runtimeSource === "string"
|
|
159
174
|
&& !runtimeSource.trimStart().startsWith("{")
|
|
@@ -187,7 +202,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
|
|
|
187
202
|
if (run.effective.workspace.root !== workspace) {
|
|
188
203
|
throw new Error("Runtime observation Hook workspace does not match the durable Run snapshot.");
|
|
189
204
|
}
|
|
190
|
-
if (session !== undefined && acceptedBinding === null) {
|
|
205
|
+
if (session !== undefined && acceptedBinding === null && !replacementStartup) {
|
|
191
206
|
if (session.adapterId !== adapterId
|
|
192
207
|
|| session.launchId !== effectiveLaunchId
|
|
193
208
|
|| session.nativeSessionId !== nativeSessionId
|
|
@@ -195,7 +210,7 @@ export function resolveRuntimeHookRunFence(environment, adapterId, payloadNative
|
|
|
195
210
|
throw new Error("Runtime observation Hook Session does not match its durable generation.");
|
|
196
211
|
}
|
|
197
212
|
}
|
|
198
|
-
else if (
|
|
213
|
+
else if (acceptedBinding === null && (session === undefined || replacementStartup)) {
|
|
199
214
|
if (!discoveredStartup && !preallocatedStartup) {
|
|
200
215
|
throw new Error("Runtime observation Hook launch is not durably reserved.");
|
|
201
216
|
}
|
|
@@ -3,6 +3,7 @@ import { callController } from "../core/controllerClient.js";
|
|
|
3
3
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
4
4
|
import { createRuntimeObservation, runtimeObservationSemanticKey } from "../runtime/runtimeObservation.js";
|
|
5
5
|
import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
|
|
6
|
+
import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
6
7
|
import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
|
|
7
8
|
import { resolveRuntimeHookRunFence } from "./runtimeHookRunFence.js";
|
|
8
9
|
let structuredSequence = 0;
|
|
@@ -256,10 +257,26 @@ async function persistAndApply(home, observations, taskId, roleName) {
|
|
|
256
257
|
const inbox = new FileRuntimeEventInbox(home);
|
|
257
258
|
for (const entry of observations)
|
|
258
259
|
inbox.enqueueObservation(entry);
|
|
260
|
+
// The immutable inbox is authoritative. During a release/update handover the
|
|
261
|
+
// old Controller is draining and the replacement is not ready yet; leave the
|
|
262
|
+
// entries for normal inbox replay instead of turning a healthy Provider Turn
|
|
263
|
+
// into a transport failure.
|
|
264
|
+
if (isForeignHandoverLockHeld(home))
|
|
265
|
+
return;
|
|
259
266
|
for (const entry of observations) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
267
|
+
let result;
|
|
268
|
+
try {
|
|
269
|
+
result = await callController(home, "runtime.observation-apply", entry, {
|
|
270
|
+
timeoutMs: 10_000
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
// Close the race where the handover begins after the first check but
|
|
275
|
+
// before this socket call. The durable entry remains pending for replay.
|
|
276
|
+
if (isForeignHandoverLockHeld(home))
|
|
277
|
+
return;
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
263
280
|
// A fast Provider can accept the initial Turn before the scheduler call
|
|
264
281
|
// that launched this Host has returned and committed `run.pushed`. The
|
|
265
282
|
// immutable inbox entry already makes that exact fenced fact durable;
|
|
@@ -14,6 +14,15 @@ export class ControllerClientError extends Error {
|
|
|
14
14
|
this.name = "ControllerClientError";
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
/** The Controller may have committed the request before this client lost its acknowledgement. */
|
|
18
|
+
export function controllerCallMayHaveApplied(error) {
|
|
19
|
+
return error instanceof ControllerClientError
|
|
20
|
+
&& [
|
|
21
|
+
"CONTROLLER_TIMEOUT",
|
|
22
|
+
"CONTROLLER_UNAVAILABLE",
|
|
23
|
+
"INVALID_RESPONSE"
|
|
24
|
+
].includes(error.code);
|
|
25
|
+
}
|
|
17
26
|
export async function readControllerDiscovery(home) {
|
|
18
27
|
const discoveryPath = join(home, CONTROLLER_DISCOVERY_PATH);
|
|
19
28
|
try {
|
|
@@ -210,6 +219,7 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
|
|
|
210
219
|
const socket = createConnection(socketPath);
|
|
211
220
|
let buffer = Buffer.alloc(0);
|
|
212
221
|
let settled = false;
|
|
222
|
+
let deliveryStarted = false;
|
|
213
223
|
const timer = setTimeout(() => {
|
|
214
224
|
fail(new ControllerClientError("CONTROLLER_TIMEOUT", "Controller request timed out."));
|
|
215
225
|
}, timeoutMs);
|
|
@@ -229,7 +239,13 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
|
|
|
229
239
|
socket.destroy();
|
|
230
240
|
reject(error);
|
|
231
241
|
};
|
|
232
|
-
socket.on("connect", () =>
|
|
242
|
+
socket.on("connect", () => {
|
|
243
|
+
// Once write begins, a missing response cannot prove that the Controller
|
|
244
|
+
// did not commit the request. Callers must use their domain identity to
|
|
245
|
+
// decide whether an explicit retry is safe.
|
|
246
|
+
deliveryStarted = true;
|
|
247
|
+
socket.write(requestLine);
|
|
248
|
+
});
|
|
233
249
|
socket.on("data", (chunk) => {
|
|
234
250
|
if (settled)
|
|
235
251
|
return;
|
|
@@ -260,7 +276,9 @@ function exchange(socketPath, requestLine, expectedId, timeoutMs) {
|
|
|
260
276
|
fail(invalidResponse());
|
|
261
277
|
});
|
|
262
278
|
socket.on("error", () => {
|
|
263
|
-
fail(
|
|
279
|
+
fail(deliveryStarted
|
|
280
|
+
? new ControllerClientError("CONTROLLER_DELIVERY_UNKNOWN", "Controller request delivery is unknown.")
|
|
281
|
+
: new ControllerClientError("CONTROLLER_UNAVAILABLE", "Controller is unavailable."));
|
|
264
282
|
});
|
|
265
283
|
});
|
|
266
284
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { hasRecentTurnId, rememberRecentTurnId, validatePendingTurnCompletion, validateRecentTurnIds } from "./turnCompletion.js";
|
|
3
3
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskSession, validateEffectiveLaunchSnapshot } from "./effectiveLaunch.js";
|
|
4
|
-
import { rebindProviderRuntimeRun, validateProviderRuntimeBinding } from "../runtime/providerRuntimeIdentity.js";
|
|
4
|
+
import { currentProviderActivation, endProviderActivation, rebindProviderRuntimeRun, validateProviderRuntimeBinding } from "../runtime/providerRuntimeIdentity.js";
|
|
5
5
|
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
6
6
|
export function createRoleSessionSet(owner, activeAgentId, now) {
|
|
7
7
|
const base = {
|
|
@@ -120,6 +120,20 @@ export function recordRoleAgentSession(set, input, now) {
|
|
|
120
120
|
validateRoleSessionSet(updated);
|
|
121
121
|
return updated;
|
|
122
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Atomically archives the quiescent old native Session and binds the Provider
|
|
125
|
+
* Conversation selected by an already-authorized switch. Authorization is
|
|
126
|
+
* deliberately owned by the Controller caller, not inferred here.
|
|
127
|
+
*/
|
|
128
|
+
export function replaceTaskRoleAgentSessionForConversationSwitch(set, input, now) {
|
|
129
|
+
validateRoleSessionSet(set);
|
|
130
|
+
const existing = set.sessions[input.agentId];
|
|
131
|
+
if (existing === undefined || existing.nativeSessionId === input.nativeSessionId) {
|
|
132
|
+
return recordRoleAgentSession(set, input, now);
|
|
133
|
+
}
|
|
134
|
+
const terminalized = updateRoleAgentSessionStatus(set, input.agentId, "stopped", now);
|
|
135
|
+
return recordRoleAgentSession(terminalized, input, now);
|
|
136
|
+
}
|
|
123
137
|
/**
|
|
124
138
|
* Session titles and previews can originate in native Agent output. Keep them
|
|
125
139
|
* single-line and inert before they are persisted or rendered in a terminal.
|
|
@@ -179,25 +193,6 @@ export function retireTaskRoleSessionsForWorkspace(set, now) {
|
|
|
179
193
|
updatedAt: timestamp
|
|
180
194
|
});
|
|
181
195
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Clears the Provider transport identity after its physical runtime is proven
|
|
184
|
-
* stopped, without retiring workspace-bound Session records. Workspace
|
|
185
|
-
* retirement remains a separate, stricter transaction after every supported
|
|
186
|
-
* placeholder has been terminalized.
|
|
187
|
-
*/
|
|
188
|
-
export function clearTaskRoleProviderRuntimeForCleanup(set, now) {
|
|
189
|
-
validateRoleSessionSet(set);
|
|
190
|
-
if (set.inFlight !== null) {
|
|
191
|
-
throw new Error("Cannot clear a Task Role Provider runtime with unsettled Run state.");
|
|
192
|
-
}
|
|
193
|
-
if (set.providerBinding === null)
|
|
194
|
-
return set;
|
|
195
|
-
return validateRoleSessionSet({
|
|
196
|
-
...set,
|
|
197
|
-
providerBinding: null,
|
|
198
|
-
updatedAt: requireDate(now, "Provider Runtime cleanup timestamp")
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
196
|
/**
|
|
202
197
|
* Terminalizes only the aggregate-16 Claude placeholder shape after the
|
|
203
198
|
* caller has fenced the Task store and proved that the exact Role has no live
|
|
@@ -307,7 +302,10 @@ export function bindTaskRoleRun(set, fence, preparedAt, mode) {
|
|
|
307
302
|
throw new Error("Task Role session set already has an in-flight Run.");
|
|
308
303
|
}
|
|
309
304
|
const timestamp = requireDate(preparedAt, "Task Role Run preparedAt");
|
|
310
|
-
|
|
305
|
+
// A fresh Conversation is a two-phase replacement: keep the old binding as
|
|
306
|
+
// current evidence until the new Provider session is observed and atomically
|
|
307
|
+
// superseded. Homes with no prior Conversation still start from null.
|
|
308
|
+
const providerBinding = set.providerBinding !== null
|
|
311
309
|
? rebindProviderRuntimeRun(set.providerBinding, normalized.runId)
|
|
312
310
|
: null;
|
|
313
311
|
const updated = {
|
|
@@ -372,6 +370,35 @@ export function updateTaskRoleProviderRuntime(set, binding, updatedAt) {
|
|
|
372
370
|
updatedAt: requireDate(updatedAt, "Provider Runtime Binding timestamp")
|
|
373
371
|
});
|
|
374
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Records an exact local Host exit without forgetting the Provider
|
|
375
|
+
* Conversation. The ended Activation releases writer authority, while the
|
|
376
|
+
* current Conversation and any unsettled Turn remain durable recovery fences.
|
|
377
|
+
*/
|
|
378
|
+
export function stopTaskRoleRuntimeAfterPhysicalExit(set, now) {
|
|
379
|
+
validateRoleSessionSet(set);
|
|
380
|
+
const active = set.sessions[set.activeAgentId];
|
|
381
|
+
if (active === undefined)
|
|
382
|
+
return set;
|
|
383
|
+
const activation = set.providerBinding === null
|
|
384
|
+
? null
|
|
385
|
+
: currentProviderActivation(set.providerBinding);
|
|
386
|
+
if (active.status === "stopped") {
|
|
387
|
+
if (activation !== null) {
|
|
388
|
+
throw new Error("Stopped Task Role Session cannot retain a live Provider Activation.");
|
|
389
|
+
}
|
|
390
|
+
return set;
|
|
391
|
+
}
|
|
392
|
+
let updated = updateRoleAgentSessionStatus(set, set.activeAgentId, "stopped", now);
|
|
393
|
+
if (activation !== null) {
|
|
394
|
+
updated = updateTaskRoleProviderRuntime(updated, endProviderActivation(updated.providerBinding, activation.activationId, {
|
|
395
|
+
status: "ended",
|
|
396
|
+
endedAt: now.toISOString(),
|
|
397
|
+
reason: "runtime-physical-exit"
|
|
398
|
+
}), now);
|
|
399
|
+
}
|
|
400
|
+
return updated;
|
|
401
|
+
}
|
|
375
402
|
export function markTaskRoleRunPushed(set, fence, pushedAt) {
|
|
376
403
|
validateRoleSessionSet(set);
|
|
377
404
|
assertTaskRoleSessionSet(set);
|
|
@@ -515,31 +542,6 @@ export function terminalizeTaskRoleRunSession(set, fence, terminalAt) {
|
|
|
515
542
|
}
|
|
516
543
|
return updated;
|
|
517
544
|
}
|
|
518
|
-
/**
|
|
519
|
-
* Resets the current native generation after its active Run is terminal.
|
|
520
|
-
* The Controller separately owns verified process cleanup.
|
|
521
|
-
*/
|
|
522
|
-
export function resetTaskRoleSession(set, now) {
|
|
523
|
-
validateRoleSessionSet(set);
|
|
524
|
-
const timestamp = requireDate(now, "Task Role Session reset timestamp");
|
|
525
|
-
const current = set.sessions[set.activeAgentId];
|
|
526
|
-
const sessions = { ...set.sessions };
|
|
527
|
-
delete sessions[set.activeAgentId];
|
|
528
|
-
const history = current === undefined
|
|
529
|
-
? set.history
|
|
530
|
-
: [
|
|
531
|
-
...(set.history ?? []),
|
|
532
|
-
{ ...current, status: "broken", updatedAt: timestamp }
|
|
533
|
-
];
|
|
534
|
-
return validateRoleSessionSet({
|
|
535
|
-
...set,
|
|
536
|
-
sessions,
|
|
537
|
-
...(history === undefined ? {} : { history }),
|
|
538
|
-
inFlight: null,
|
|
539
|
-
providerBinding: null,
|
|
540
|
-
updatedAt: timestamp
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
545
|
export function settleTaskRoleCompletion(set, expected, settledAt) {
|
|
544
546
|
validateRoleSessionSet(set);
|
|
545
547
|
assertTaskRoleSessionSet(set);
|
|
@@ -12,7 +12,9 @@ import { compileRoleSessionContext, roleSessionKind } from "../context/roleSessi
|
|
|
12
12
|
import { materializeSessionBootstrap } from "../context/sessionBootstrapManifest.js";
|
|
13
13
|
import { serializeRunBootstrapEnvelope, serializeRunHostRecoveryEnvelope } from "../context/runContextContract.js";
|
|
14
14
|
import { serializeProviderRetryEnvelope } from "../run/providerRetry.js";
|
|
15
|
+
import { serializeWorkflowOutcomeRequestEnvelope } from "../run/runControlRequest.js";
|
|
15
16
|
import { prefixYuiTitleInput } from "../run/runIdentity.js";
|
|
17
|
+
import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
|
|
16
18
|
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
17
19
|
import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
18
20
|
import { nativeSessionIdForLaunch } from "../runtime/preallocatedNativeSession.js";
|
|
@@ -25,7 +27,8 @@ import { parseTaskRuntimeIsolationDescriptor, taskRuntimeIsolationEnvironment }
|
|
|
25
27
|
import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
26
28
|
import { builtinAgentDriverRegistry, builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
27
29
|
import { managedRuntimeAdmission } from "../runtime/agentDriver.js";
|
|
28
|
-
import {
|
|
30
|
+
import { freshConversationLaunchAllowed } from "../runtime/conversationSwitch.js";
|
|
31
|
+
import { currentProviderActivation } from "../runtime/providerRuntimeIdentity.js";
|
|
29
32
|
import { assertCodexLaunchOverridesAvailable, inspectCodexLaunchConfig } from "./codexConfigConflict.js";
|
|
30
33
|
/** Builds managed native Agent launches from the authoritative Task records. */
|
|
31
34
|
export class FileRoleLaunchPlanner {
|
|
@@ -224,6 +227,20 @@ export class FileRoleLaunchPlanner {
|
|
|
224
227
|
if (input.mode === "resume" && !compatibleExisting) {
|
|
225
228
|
throw new Error(`Task Role resume effective snapshot drifted: ${task.id}/${role.name}.`);
|
|
226
229
|
}
|
|
230
|
+
if (input.mode === "new" && sessionSet !== null
|
|
231
|
+
&& !freshConversationLaunchAllowed({
|
|
232
|
+
sessions: sessionSet,
|
|
233
|
+
events: this.store.listEvents(task.id),
|
|
234
|
+
mailbox: this.store.getWorkMailbox({
|
|
235
|
+
kind: "role",
|
|
236
|
+
taskId: task.id,
|
|
237
|
+
roleName: role.name
|
|
238
|
+
}),
|
|
239
|
+
roleName: role.name,
|
|
240
|
+
...(input.runId === undefined ? {} : { candidateRunId: input.runId })
|
|
241
|
+
})) {
|
|
242
|
+
throw new Error(`Fresh Provider Conversation is not authorized: ${task.id}/${role.name}.`);
|
|
243
|
+
}
|
|
227
244
|
return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, runWorkspace, effective, {
|
|
228
245
|
purpose: activeRun?.purpose ?? "execution"
|
|
229
246
|
});
|
|
@@ -464,10 +481,10 @@ export class FileRoleLaunchPlanner {
|
|
|
464
481
|
if (runtimeDescriptor !== undefined && runtimeDescriptorSource !== undefined) {
|
|
465
482
|
this.#writeExactTaskRuntimeDescriptor(runtimeDescriptor, runtimeDescriptorSource);
|
|
466
483
|
}
|
|
467
|
-
//
|
|
468
|
-
// task-scope launch
|
|
469
|
-
//
|
|
470
|
-
//
|
|
484
|
+
// Generate a candidate per-Provider-process DurableJob caller key for
|
|
485
|
+
// every task-scope launch. A reused live Conversation keeps the key that
|
|
486
|
+
// its process inherited; a new Host or a Conversation replacement commits
|
|
487
|
+
// the fresh candidate only after Provider dispatch is observed.
|
|
471
488
|
let jobCallerKey;
|
|
472
489
|
if (owner.scope === "task" && (input.mode === "new" || input.mode === "resume")) {
|
|
473
490
|
jobCallerKey = randomBytes(32).toString("hex");
|
|
@@ -476,35 +493,62 @@ export class FileRoleLaunchPlanner {
|
|
|
476
493
|
// write, whether it creates or resumes the native Conversation. Keeping
|
|
477
494
|
// resume on the same launch handshake prevents a durable AgentRun from
|
|
478
495
|
// existing without a corresponding Provider Turn.
|
|
479
|
-
|
|
480
|
-
|
|
496
|
+
if (managedControl && (managedRun === null || managedRun.status !== "active")) {
|
|
497
|
+
throw new Error(`Managed Run is no longer active: ${input.runId}.`);
|
|
498
|
+
}
|
|
499
|
+
const carriesInitialTurn = managedControl && (managedRun.pushedAt === undefined
|
|
500
|
+
|| managedRun.providerRetry?.state === "dispatching"
|
|
501
|
+
|| managedRun.controlRequest?.state === "dispatching");
|
|
481
502
|
const providerAuthority = managedControl
|
|
482
|
-
? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId)
|
|
503
|
+
? this.#providerAuthorityForLaunch(owner.taskId, role.name, input.launchId, input.mode)
|
|
483
504
|
: undefined;
|
|
484
505
|
const providerNativeSessionId = binding.adapterId === "claude"
|
|
485
506
|
? preallocatedNativeSessionId
|
|
486
507
|
: resumeNativeSessionId;
|
|
487
|
-
const
|
|
508
|
+
const initialTurn = carriesInitialTurn
|
|
488
509
|
? {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
transport: managedCompiled.transport,
|
|
492
|
-
mode: resumeNativeSessionId === undefined ? "new" : "resume",
|
|
493
|
-
...(providerNativeSessionId === undefined
|
|
494
|
-
? {}
|
|
495
|
-
: { nativeSessionId: providerNativeSessionId }),
|
|
496
|
-
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
497
|
-
authority: providerAuthority,
|
|
498
|
-
...(carriesInitialTurn
|
|
499
|
-
? {
|
|
500
|
-
initialTurn: {
|
|
501
|
-
attemptId: formatAgentRunReceiptId(owner.taskId, input.runId),
|
|
502
|
-
boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionTitle)
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
: {})
|
|
510
|
+
attemptId: agentRunDeliveryReceiptId(managedRun),
|
|
511
|
+
boundedText: managedRunLaunchEnvelope(managedRun, input.mode, sessionTitle)
|
|
506
512
|
}
|
|
507
513
|
: undefined;
|
|
514
|
+
const providerControl = !managedControl
|
|
515
|
+
? undefined
|
|
516
|
+
: initialTurn === undefined
|
|
517
|
+
? {
|
|
518
|
+
schemaVersion: 1,
|
|
519
|
+
adapterId: binding.adapterId,
|
|
520
|
+
transport: managedCompiled.transport,
|
|
521
|
+
kind: "ensure",
|
|
522
|
+
mode: "resume",
|
|
523
|
+
nativeSessionId: requireText(providerNativeSessionId, "Managed Provider ensure native session id"),
|
|
524
|
+
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
525
|
+
authority: providerAuthority
|
|
526
|
+
}
|
|
527
|
+
: resumeNativeSessionId === undefined
|
|
528
|
+
? {
|
|
529
|
+
schemaVersion: 1,
|
|
530
|
+
adapterId: binding.adapterId,
|
|
531
|
+
transport: managedCompiled.transport,
|
|
532
|
+
kind: "new",
|
|
533
|
+
mode: "new",
|
|
534
|
+
...(providerNativeSessionId === undefined
|
|
535
|
+
? {}
|
|
536
|
+
: { nativeSessionId: providerNativeSessionId }),
|
|
537
|
+
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
538
|
+
authority: providerAuthority,
|
|
539
|
+
initialTurn
|
|
540
|
+
}
|
|
541
|
+
: {
|
|
542
|
+
schemaVersion: 1,
|
|
543
|
+
adapterId: binding.adapterId,
|
|
544
|
+
transport: managedCompiled.transport,
|
|
545
|
+
kind: "resume",
|
|
546
|
+
mode: "resume",
|
|
547
|
+
nativeSessionId: requireText(providerNativeSessionId, "Managed Provider resume native session id"),
|
|
548
|
+
...(sessionTitle === undefined ? {} : { sessionTitle }),
|
|
549
|
+
authority: providerAuthority,
|
|
550
|
+
initialTurn
|
|
551
|
+
};
|
|
508
552
|
const launch = {
|
|
509
553
|
command,
|
|
510
554
|
args,
|
|
@@ -572,13 +616,26 @@ export class FileRoleLaunchPlanner {
|
|
|
572
616
|
: {})
|
|
573
617
|
};
|
|
574
618
|
}
|
|
575
|
-
#providerAuthorityForLaunch(taskId, roleName, launchId) {
|
|
619
|
+
#providerAuthorityForLaunch(taskId, roleName, launchId, mode) {
|
|
576
620
|
const activationId = requireText(launchId, "Managed Provider Activation id");
|
|
577
621
|
const binding = this.store.getTaskRoleSessionSet(taskId, roleName)?.providerBinding;
|
|
578
622
|
if (binding === null || binding === undefined) {
|
|
579
623
|
return { epoch: 1, owner: "controller", holderId: activationId };
|
|
580
624
|
}
|
|
581
625
|
if (binding.authority.owner === "controller") {
|
|
626
|
+
if (mode === "new") {
|
|
627
|
+
const activation = currentProviderActivation(binding);
|
|
628
|
+
if (activation === null || binding.authority.holderId !== activation.activationId) {
|
|
629
|
+
throw new Error(`Provider Activation is not exact: ${taskId}/${roleName}.`);
|
|
630
|
+
}
|
|
631
|
+
// An actor-requested switch first ends the idle old Activation (+1)
|
|
632
|
+
// and then binds the replacement Activation (+1).
|
|
633
|
+
return {
|
|
634
|
+
epoch: binding.authority.epoch + 2,
|
|
635
|
+
owner: "controller",
|
|
636
|
+
holderId: activationId
|
|
637
|
+
};
|
|
638
|
+
}
|
|
582
639
|
return {
|
|
583
640
|
epoch: binding.authority.epoch,
|
|
584
641
|
owner: "controller",
|
|
@@ -642,9 +699,16 @@ function managedRunLaunchEnvelope(run, mode, title) {
|
|
|
642
699
|
roleName: run.roleName,
|
|
643
700
|
retry: run.providerRetry
|
|
644
701
|
})
|
|
645
|
-
:
|
|
646
|
-
?
|
|
647
|
-
|
|
702
|
+
: run.controlRequest?.state === "dispatching"
|
|
703
|
+
? serializeWorkflowOutcomeRequestEnvelope({
|
|
704
|
+
taskId: run.taskId,
|
|
705
|
+
runId: run.id,
|
|
706
|
+
roleName: run.roleName,
|
|
707
|
+
request: run.controlRequest
|
|
708
|
+
})
|
|
709
|
+
: mode === "resume" && run.pushedAt !== undefined
|
|
710
|
+
? serializeRunHostRecoveryEnvelope(run.bootstrapEnvelope)
|
|
711
|
+
: serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
|
|
648
712
|
return title === undefined
|
|
649
713
|
? body
|
|
650
714
|
: prefixYuiTitleInput(body, title);
|