@zq-silk/yui 0.8.1 → 0.8.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/ARCHITECTURE.md +27 -28
- package/README.md +35 -46
- package/dist/cli/commandCatalog.js +49 -14
- package/dist/cli/interactionPolicy.js +4 -10
- package/dist/cli/invocationRouter.js +2 -1
- package/dist/cli.js +73 -21
- package/dist/commands/taskCommands.js +108 -53
- package/dist/controller/fileSchedulerStoreAdapter.js +389 -70
- package/dist/controller/resourceInventory.js +9 -5
- package/dist/controller/runtime.js +80 -7
- package/dist/controller/runtimeLaunchCoordinator.js +18 -78
- package/dist/controller/structuredProviderObservation.js +273 -0
- package/dist/executor/agentAdapter.js +40 -0
- package/dist/executor/agentExecutor.js +31 -7
- package/dist/executor/executorRegistry.js +11 -49
- package/dist/executor/fileRoleLaunchPlanner.js +115 -37
- package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
- package/dist/run/agentRun.js +2 -2
- package/dist/runtime/agentHost.js +767 -158
- package/dist/runtime/builtinAgentDrivers.js +1 -5
- package/dist/runtime/codexAppServerRuntime.js +67 -60
- package/dist/runtime/exactControlPlane.js +7 -2
- package/dist/runtime/index.js +6 -2
- package/dist/runtime/launchBroker.js +30 -8
- package/dist/runtime/providerAuthorityFence.js +24 -0
- package/dist/runtime/providerControl.js +63 -0
- package/dist/runtime/providerRecoveryDecision.js +55 -0
- package/dist/runtime/providerRuntimeIdentity.js +269 -19
- package/dist/runtime/runtimeBinding.js +20 -11
- package/dist/runtime/structuredProviderHost.js +476 -0
- package/dist/runtime/tmuxAdapters.js +143 -42
- package/dist/scheduler/activeRoleRunDelivery.js +206 -120
- package/dist/scheduler/leaderWakeupProcessor.js +141 -16
- package/dist/storage/migration/productionRegistry.js +111 -0
- package/dist/storage/taskStore.js +1 -1
- package/dist/tmux/tmuxManager.js +1 -1
- package/i18n/README.zh-CN.md +11 -8
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ export function builtinDriverIdForAdapter(adapterId) {
|
|
|
7
7
|
return builtinAgentDriverRegistry().requireByAdapterId(adapterId).id;
|
|
8
8
|
}
|
|
9
9
|
const STRUCTURED_CLI_CAPABILITIES = Object.freeze({
|
|
10
|
-
surfaces: Object.freeze(["interactive-cli"]),
|
|
10
|
+
surfaces: Object.freeze(["interactive-cli", "managed-protocol"]),
|
|
11
11
|
lifecycle: Object.freeze({
|
|
12
12
|
host: "persistent",
|
|
13
13
|
providerProcess: "persistent",
|
|
@@ -67,10 +67,6 @@ export const BUILTIN_AGENT_DRIVERS = Object.freeze([
|
|
|
67
67
|
adapterId: "claude",
|
|
68
68
|
capabilities: Object.freeze({
|
|
69
69
|
...STRUCTURED_CLI_CAPABILITIES,
|
|
70
|
-
lifecycle: Object.freeze({
|
|
71
|
-
...STRUCTURED_CLI_CAPABILITIES.lifecycle,
|
|
72
|
-
providerProcess: "per-turn"
|
|
73
|
-
}),
|
|
74
70
|
observation: Object.freeze({
|
|
75
71
|
...STRUCTURED_CLI_CAPABILITIES.observation,
|
|
76
72
|
sessionBootstrap: "preallocated",
|
|
@@ -15,6 +15,7 @@ export class CodexAppServerRequestError extends Error {
|
|
|
15
15
|
*/
|
|
16
16
|
export class CodexAppServerRuntime {
|
|
17
17
|
transport;
|
|
18
|
+
providerNamespace = "openai/codex";
|
|
18
19
|
constructor(transport) {
|
|
19
20
|
this.transport = transport;
|
|
20
21
|
}
|
|
@@ -46,18 +47,59 @@ export class CodexAppServerRuntime {
|
|
|
46
47
|
});
|
|
47
48
|
return parseThreadSnapshot(result, id, "unknown");
|
|
48
49
|
}
|
|
50
|
+
async inspectConversation(conversationId) {
|
|
51
|
+
const id = text(conversationId, "Codex thread id");
|
|
52
|
+
try {
|
|
53
|
+
const snapshot = await this.readConversation(id);
|
|
54
|
+
return {
|
|
55
|
+
state: "exists",
|
|
56
|
+
conversationId: snapshot.threadId,
|
|
57
|
+
...(snapshot.activeTurnId === undefined ? {} : { activeTurnId: snapshot.activeTurnId })
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
state: codexAppServerErrorIsMissing(error) ? "missing" : "unknown",
|
|
63
|
+
conversationId: id
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async submitTurn(input) {
|
|
68
|
+
return this.startTurn({
|
|
69
|
+
conversationId: input.conversationId,
|
|
70
|
+
text: input.text,
|
|
71
|
+
expectedNoActiveTurn: input.expectedNoActiveTurn,
|
|
72
|
+
clientUserMessageId: input.attemptId
|
|
73
|
+
});
|
|
74
|
+
}
|
|
49
75
|
async startTurn(input) {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
76
|
+
const requestedThreadId = text(input.conversationId, "Codex thread id");
|
|
77
|
+
let threadId = requestedThreadId;
|
|
78
|
+
try {
|
|
79
|
+
const snapshot = await this.readConversation(requestedThreadId);
|
|
80
|
+
threadId = snapshot.threadId;
|
|
81
|
+
if (input.expectedNoActiveTurn && snapshot.activeTurnId !== undefined) {
|
|
82
|
+
return { status: "not-accepted", reason: `active-turn:${snapshot.activeTurnId}` };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// A new App Server thread has no materialized Turn history yet. This
|
|
87
|
+
// exact response proves there cannot be an active Turn, so its first
|
|
88
|
+
// mutation can proceed without weakening unknown-delivery handling.
|
|
89
|
+
if (!codexAppServerErrorIsUnmaterialized(error))
|
|
90
|
+
throw error;
|
|
53
91
|
}
|
|
54
92
|
try {
|
|
55
93
|
const result = await this.transport.request("turn/start", {
|
|
56
|
-
threadId
|
|
94
|
+
threadId,
|
|
57
95
|
...(input.clientUserMessageId === undefined
|
|
58
96
|
? {}
|
|
59
97
|
: { clientUserMessageId: text(input.clientUserMessageId, "Codex input attempt id") }),
|
|
60
|
-
input: [{
|
|
98
|
+
input: [{
|
|
99
|
+
type: "text",
|
|
100
|
+
text: text(input.text, "Codex Turn input"),
|
|
101
|
+
text_elements: []
|
|
102
|
+
}]
|
|
61
103
|
});
|
|
62
104
|
const turnId = optionalId(result.turnId)
|
|
63
105
|
?? optionalId(objectMember(result, "turn")?.id);
|
|
@@ -88,7 +130,11 @@ export class CodexAppServerRuntime {
|
|
|
88
130
|
...(input.clientUserMessageId === undefined
|
|
89
131
|
? {}
|
|
90
132
|
: { clientUserMessageId: text(input.clientUserMessageId, "Codex input attempt id") }),
|
|
91
|
-
input: [{
|
|
133
|
+
input: [{
|
|
134
|
+
type: "text",
|
|
135
|
+
text: text(input.text, "Codex steer input"),
|
|
136
|
+
text_elements: []
|
|
137
|
+
}]
|
|
92
138
|
});
|
|
93
139
|
const acceptedTurnId = optionalId(result.turnId);
|
|
94
140
|
if (acceptedTurnId === expectedTurnId)
|
|
@@ -174,48 +220,6 @@ export class CodexAppServerRuntime {
|
|
|
174
220
|
}
|
|
175
221
|
return { quality: "exact", continuations: Object.freeze(observed) };
|
|
176
222
|
}
|
|
177
|
-
async route(input) {
|
|
178
|
-
if (input.binding.adapterId !== "codex"
|
|
179
|
-
|| input.binding.nativeSessionId !== input.fence.conversationId
|
|
180
|
-
|| input.binding.launchId !== input.fence.activationId)
|
|
181
|
-
return "unsafe";
|
|
182
|
-
if (input.mode === "inject") {
|
|
183
|
-
return this.injectItems({
|
|
184
|
-
conversationId: input.fence.conversationId,
|
|
185
|
-
text: input.text
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
if (input.fence.nativeTurnId === undefined)
|
|
189
|
-
return "unsafe";
|
|
190
|
-
const outcome = await this.steerTurn({
|
|
191
|
-
conversationId: input.fence.conversationId,
|
|
192
|
-
expectedTurnId: input.fence.nativeTurnId,
|
|
193
|
-
text: input.text,
|
|
194
|
-
clientUserMessageId: input.attemptId
|
|
195
|
-
});
|
|
196
|
-
return outcome.status === "accepted" ? "accepted"
|
|
197
|
-
: outcome.status === "not-accepted" ? "not-accepted"
|
|
198
|
-
: "unknown";
|
|
199
|
-
}
|
|
200
|
-
async reconcile(input) {
|
|
201
|
-
if (input.binding.adapterId !== "codex"
|
|
202
|
-
|| input.binding.nativeSessionId !== input.fence.conversationId
|
|
203
|
-
|| input.binding.launchId !== input.fence.activationId)
|
|
204
|
-
return "unavailable";
|
|
205
|
-
// inject_items has no client receipt/idempotency field in the App Server
|
|
206
|
-
// protocol. A lost response therefore remains unknown and is never resent.
|
|
207
|
-
if (input.mode === "inject")
|
|
208
|
-
return "unknown";
|
|
209
|
-
try {
|
|
210
|
-
const snapshot = await this.readConversation(input.fence.conversationId);
|
|
211
|
-
return threadContainsClientInput(snapshot.raw, input.attemptId)
|
|
212
|
-
? "accepted"
|
|
213
|
-
: "not-accepted";
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
return isNotLoaded(error) ? "unavailable" : "unknown";
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
223
|
}
|
|
220
224
|
/** thread/closed means the loaded Activation ended; the durable thread remains resumable. */
|
|
221
225
|
export function codexNotificationBoundary(input) {
|
|
@@ -305,18 +309,6 @@ function optionalTurnStatus(value) {
|
|
|
305
309
|
? value
|
|
306
310
|
: undefined;
|
|
307
311
|
}
|
|
308
|
-
function threadContainsClientInput(raw, attemptId) {
|
|
309
|
-
const thread = objectMember(raw, "thread") ?? raw;
|
|
310
|
-
const turns = Array.isArray(thread.turns) ? thread.turns : [];
|
|
311
|
-
return turns.some((rawTurn) => {
|
|
312
|
-
const turn = object(rawTurn);
|
|
313
|
-
const items = turn === null || !Array.isArray(turn.items) ? [] : turn.items;
|
|
314
|
-
return items.some((rawItem) => {
|
|
315
|
-
const item = object(rawItem);
|
|
316
|
-
return item?.type === "userMessage" && item.clientId === attemptId;
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
312
|
function classifyMutationError(error) {
|
|
321
313
|
if (error instanceof CodexAppServerRequestError) {
|
|
322
314
|
if (["INVALID_PARAMS", "NOT_FOUND", "TURN_NOT_ACTIVE", -32602].includes(error.code)) {
|
|
@@ -330,6 +322,21 @@ function isNotLoaded(error) {
|
|
|
330
322
|
&& (String(error.code).toLowerCase().includes("not_loaded")
|
|
331
323
|
|| error.message.toLowerCase().includes("not loaded"));
|
|
332
324
|
}
|
|
325
|
+
function codexAppServerErrorIsUnmaterialized(error) {
|
|
326
|
+
if (!(error instanceof CodexAppServerRequestError) || Number(error.code) !== -32600) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
return /\bthread\b.*\bnot materialized yet\b.*\bbefore first user message\b/iu.test(error.message);
|
|
330
|
+
}
|
|
331
|
+
export function codexAppServerErrorIsMissing(error) {
|
|
332
|
+
if (!(error instanceof CodexAppServerRequestError))
|
|
333
|
+
return false;
|
|
334
|
+
if (error.code === "NOT_FOUND")
|
|
335
|
+
return true;
|
|
336
|
+
const message = error.message.toLowerCase();
|
|
337
|
+
return /\b(thread|conversation)\b.*\b(not found|missing|does not exist)\b/u.test(message)
|
|
338
|
+
|| /\b(not found|missing|does not exist)\b.*\b(thread|conversation)\b/u.test(message);
|
|
339
|
+
}
|
|
333
340
|
function threadId(result) {
|
|
334
341
|
return text(optionalId(result.threadId) ?? optionalId(objectMember(result, "thread")?.id), "Codex thread id");
|
|
335
342
|
}
|
|
@@ -361,8 +361,13 @@ export function refreshReusedTaskRuntimeDescriptorSource(source, home, store, cu
|
|
|
361
361
|
throw new Error("A managed Task runtime descriptor must use its stable file source.");
|
|
362
362
|
}
|
|
363
363
|
const previous = resolved.descriptor;
|
|
364
|
-
if (previous.nativeSessionId === undefined
|
|
365
|
-
|| previous.
|
|
364
|
+
if (previous.nativeSessionId === undefined) {
|
|
365
|
+
if (previous.runId !== current.runId || previous.launchId !== current.launchId) {
|
|
366
|
+
throw new Error("Task runtime descriptor source cannot bind a native Session from another generation.");
|
|
367
|
+
}
|
|
368
|
+
return refreshExactTaskRuntimeDescriptorSource(source, home, store);
|
|
369
|
+
}
|
|
370
|
+
if (previous.nativeSessionId !== current.nativeSessionId) {
|
|
366
371
|
throw new Error("Task runtime descriptor source cannot jump to a replacement native Session.");
|
|
367
372
|
}
|
|
368
373
|
const refreshed = createExactTaskRuntimeDescriptor({
|
package/dist/runtime/index.js
CHANGED
|
@@ -5,12 +5,16 @@ export { normalizeRuntimeOwner } from "./runtimeOwner.js";
|
|
|
5
5
|
export { createSessionLaunchRequest } from "./sessionLaunchRequest.js";
|
|
6
6
|
export { createPendingTurnCompletion, DEFAULT_RECENT_TURN_ID_LIMIT, hasRecentTurnId, rememberRecentTurnId, validatePendingTurnCompletion, validateRecentTurnIds } from "./turnCompletion.js";
|
|
7
7
|
export { RuntimeHostContentionError, RuntimeLaunchError } from "./ports.js";
|
|
8
|
-
export {
|
|
8
|
+
export { AgentHostPromptPushAdapter, TmuxSessionHost } from "./tmuxAdapters.js";
|
|
9
9
|
export { FileTaskRuntimeIsolation, YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR, YUI_TASK_RUNTIME_SERVICE_NAMESPACE, assertTaskRuntimeIsolationPreflight, createTaskRuntimeIsolationDescriptor, parseTaskRuntimeIsolationDescriptor, planTaskRuntimeCleanup, taskRuntimeIsolationEnvironment, taskRuntimeIsolationFingerprint } from "./taskRuntimeIsolation.js";
|
|
10
10
|
export { createSessionOwnerIdentity, discoverProviderRootByLaunchEnv, isLinuxProcessLive, listLaunchFencedProcesses, listOwnedProcessTree, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
|
|
11
11
|
export { FileSessionOwnerRegistry } from "./sessionOwnerRegistry.js";
|
|
12
12
|
export { formatRuntimeLaunchDiagnostic, redactLaunchArgument, redactLaunchText, RuntimeLaunchFailure, toRuntimeLaunchFailure } from "./launchDiagnostics.js";
|
|
13
13
|
export { DEFAULT_FORCED_GRACE_MS, DEFAULT_GRACEFUL_GRACE_MS, terminateSessionOwners } from "./sessionTerminationGuard.js";
|
|
14
14
|
export { ProviderContinuationReconciliationService } from "./providerContinuationReconciliationService.js";
|
|
15
|
-
export { codexNotificationBoundary, CodexAppServerRequestError, CodexAppServerRuntime } from "./codexAppServerRuntime.js";
|
|
15
|
+
export { codexNotificationBoundary, codexAppServerErrorIsMissing, CodexAppServerRequestError, CodexAppServerRuntime } from "./codexAppServerRuntime.js";
|
|
16
|
+
export { createProviderRuntimeBinding, acceptProviderTurn, beginProviderTurn, currentProviderActivation, currentProviderAuthority, currentProviderConversation, endProviderActivation, markProviderTurnDeliveryUnknown, rebindProviderRuntimeRun, rejectProviderTurn, settleProviderTurn, startProviderActivation, supersedeProviderConversation, transferProviderAuthority, updateProviderConversationRecoverability, validateProviderRuntimeBinding } from "./providerRuntimeIdentity.js";
|
|
17
|
+
export { FencedProviderControl } from "./providerControl.js";
|
|
18
|
+
export { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
|
|
19
|
+
export { decideProviderRecovery } from "./providerRecoveryDecision.js";
|
|
16
20
|
export { reconcileSessionOwners } from "./sessionReconciliation.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { validateProviderAuthorityFence } from "./providerAuthorityFence.js";
|
|
3
4
|
const brokers = new Map();
|
|
4
5
|
const TICKET_TTL_MS = 60_000;
|
|
5
6
|
/** One Controller-process broker per canonical Home. Payloads never hit disk or tmux. */
|
|
@@ -75,17 +76,38 @@ function validatePayload(payload) {
|
|
|
75
76
|
if (payload.startMode !== "provider" && payload.startMode !== "idle") {
|
|
76
77
|
throw new Error("Agent Host start mode is invalid.");
|
|
77
78
|
}
|
|
78
|
-
if (payload.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
if (payload.providerControl !== undefined)
|
|
80
|
+
validateProviderControl(payload.providerControl);
|
|
81
|
+
return payload;
|
|
82
|
+
}
|
|
83
|
+
function validateProviderControl(control) {
|
|
84
|
+
if (control.schemaVersion !== 1)
|
|
85
|
+
throw new Error("Agent Host Provider control version is invalid.");
|
|
86
|
+
if (control.adapterId !== "codex" && control.adapterId !== "claude") {
|
|
87
|
+
throw new Error("Agent Host Provider control adapter is invalid.");
|
|
88
|
+
}
|
|
89
|
+
if ((control.adapterId === "codex" && control.transport !== "codex-app-server-stdio")
|
|
90
|
+
|| (control.adapterId === "claude" && control.transport !== "claude-stream-json")) {
|
|
91
|
+
throw new Error("Agent Host Provider control transport does not match its adapter.");
|
|
92
|
+
}
|
|
93
|
+
if (control.mode !== "new" && control.mode !== "resume") {
|
|
94
|
+
throw new Error("Agent Host Provider control mode is invalid.");
|
|
95
|
+
}
|
|
96
|
+
const requiresNativeSessionId = control.mode === "resume" || control.adapterId === "claude";
|
|
97
|
+
if (requiresNativeSessionId !== (control.nativeSessionId !== undefined)) {
|
|
98
|
+
throw new Error("Agent Host Provider resume identity is inconsistent.");
|
|
99
|
+
}
|
|
100
|
+
if (control.nativeSessionId !== undefined)
|
|
101
|
+
text(control.nativeSessionId, "nativeSessionId");
|
|
102
|
+
validateProviderAuthorityFence(control.authority);
|
|
103
|
+
if (control.initialTurn !== undefined) {
|
|
104
|
+
text(control.initialTurn.attemptId, "Provider input attemptId");
|
|
105
|
+
if (typeof control.initialTurn.boundedText !== "string"
|
|
106
|
+
|| control.initialTurn.boundedText.includes("\0")
|
|
107
|
+
|| Buffer.byteLength(control.initialTurn.boundedText, "utf8") > 32 * 1024) {
|
|
85
108
|
throw new Error("Agent Host Provider input must be bounded bootstrap text.");
|
|
86
109
|
}
|
|
87
110
|
}
|
|
88
|
-
return payload;
|
|
89
111
|
}
|
|
90
112
|
function text(value, label) {
|
|
91
113
|
if (typeof value !== "string" || value.length === 0 || value.includes("\0")) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function validateProviderAuthorityFence(value) {
|
|
2
|
+
if (!Number.isSafeInteger(value.epoch) || value.epoch < 1) {
|
|
3
|
+
throw new Error("Provider authority epoch is invalid.");
|
|
4
|
+
}
|
|
5
|
+
if (value.owner !== "controller" && value.owner !== "human") {
|
|
6
|
+
throw new Error("Provider authority owner is invalid for a writer fence.");
|
|
7
|
+
}
|
|
8
|
+
if (typeof value.holderId !== "string" || value.holderId.trim().length === 0
|
|
9
|
+
|| value.holderId.includes("\0")) {
|
|
10
|
+
throw new Error("Provider authority holder is invalid.");
|
|
11
|
+
}
|
|
12
|
+
return Object.freeze({
|
|
13
|
+
epoch: value.epoch,
|
|
14
|
+
owner: value.owner,
|
|
15
|
+
holderId: value.holderId.trim()
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function sameProviderAuthorityFence(left, right) {
|
|
19
|
+
const first = validateProviderAuthorityFence(left);
|
|
20
|
+
const second = validateProviderAuthorityFence(right);
|
|
21
|
+
return first.epoch === second.epoch
|
|
22
|
+
&& first.owner === second.owner
|
|
23
|
+
&& first.holderId === second.holderId;
|
|
24
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { currentProviderActivation, currentProviderAuthority, currentProviderConversation, validateProviderRuntimeBinding } from "./providerRuntimeIdentity.js";
|
|
2
|
+
/**
|
|
3
|
+
* Applies the durable single-writer fence before any Provider mutation. The
|
|
4
|
+
* Adapter never receives authority to infer or repair identity.
|
|
5
|
+
*/
|
|
6
|
+
export class FencedProviderControl {
|
|
7
|
+
adapter;
|
|
8
|
+
constructor(adapter) {
|
|
9
|
+
this.adapter = adapter;
|
|
10
|
+
}
|
|
11
|
+
async submitTurn(input) {
|
|
12
|
+
this.#assertWriter(input.binding, input.fence);
|
|
13
|
+
return this.adapter.submitTurn({
|
|
14
|
+
conversationId: input.fence.conversationId,
|
|
15
|
+
attemptId: identity(input.attemptId, "Provider input attempt id"),
|
|
16
|
+
text: text(input.text, "Provider Turn input"),
|
|
17
|
+
expectedNoActiveTurn: input.expectedNoActiveTurn ?? true
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
async interruptTurn(input) {
|
|
21
|
+
this.#assertWriter(input.binding, input.fence);
|
|
22
|
+
return this.adapter.interruptTurn({
|
|
23
|
+
conversationId: input.fence.conversationId,
|
|
24
|
+
turnId: identity(input.turnId, "Provider Turn id")
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async inspectConversation(binding) {
|
|
28
|
+
const normalized = validateProviderRuntimeBinding(binding);
|
|
29
|
+
this.#assertNamespace(normalized);
|
|
30
|
+
return this.adapter.inspectConversation(currentProviderConversation(normalized).conversationId);
|
|
31
|
+
}
|
|
32
|
+
#assertWriter(binding, fence) {
|
|
33
|
+
const normalized = validateProviderRuntimeBinding(binding);
|
|
34
|
+
this.#assertNamespace(normalized);
|
|
35
|
+
const conversation = currentProviderConversation(normalized);
|
|
36
|
+
const activation = currentProviderActivation(normalized);
|
|
37
|
+
const authority = currentProviderAuthority(normalized);
|
|
38
|
+
if (conversation.conversationId !== fence.conversationId
|
|
39
|
+
|| activation?.activationId !== fence.activationId
|
|
40
|
+
|| authority.epoch !== fence.authorityEpoch
|
|
41
|
+
|| authority.owner !== fence.authorityOwner
|
|
42
|
+
|| authority.holderId !== fence.holderId) {
|
|
43
|
+
throw new Error("Provider writer fence is stale.");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
#assertNamespace(binding) {
|
|
47
|
+
if (binding.providerNamespace !== this.adapter.providerNamespace) {
|
|
48
|
+
throw new Error("Provider control Adapter does not match its Runtime Binding.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function identity(value, label) {
|
|
53
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
|
|
54
|
+
throw new Error(`${label} is invalid.`);
|
|
55
|
+
}
|
|
56
|
+
return value.trim();
|
|
57
|
+
}
|
|
58
|
+
function text(value, label) {
|
|
59
|
+
if (typeof value !== "string" || value.trim().length === 0 || value.includes("\0")) {
|
|
60
|
+
throw new Error(`${label} is invalid.`);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { currentProviderActivation, currentProviderConversation, validateProviderRuntimeBinding } from "./providerRuntimeIdentity.js";
|
|
2
|
+
/**
|
|
3
|
+
* Exact tri-state recovery policy. Unknown is a terminal decision for the
|
|
4
|
+
* automatic recovery attempt, not permission to create another Conversation.
|
|
5
|
+
*/
|
|
6
|
+
export function decideProviderRecovery(input) {
|
|
7
|
+
const binding = validateProviderRuntimeBinding(input.binding);
|
|
8
|
+
const conversation = currentProviderConversation(binding);
|
|
9
|
+
if (input.probe.conversationId !== conversation.conversationId) {
|
|
10
|
+
throw new Error("Provider recovery probe targets a different Conversation.");
|
|
11
|
+
}
|
|
12
|
+
if (input.probe.state === "unknown") {
|
|
13
|
+
return {
|
|
14
|
+
action: "attention",
|
|
15
|
+
conversationId: conversation.conversationId,
|
|
16
|
+
reason: "Provider Conversation existence is unknown; replacement is fenced."
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (input.probe.state === "exists") {
|
|
20
|
+
if (input.probe.activeTurnId !== undefined) {
|
|
21
|
+
return {
|
|
22
|
+
action: "observe-active-turn",
|
|
23
|
+
conversationId: conversation.conversationId,
|
|
24
|
+
turnId: input.probe.activeTurnId
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (input.unsettledInputDelivery || providerTurnIsUnsettled(binding)) {
|
|
28
|
+
return {
|
|
29
|
+
action: "attention",
|
|
30
|
+
conversationId: conversation.conversationId,
|
|
31
|
+
reason: "Provider Conversation exists but prior input delivery is still unsettled."
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return { action: "resume", conversationId: conversation.conversationId };
|
|
35
|
+
}
|
|
36
|
+
if (input.unsettledInputDelivery || providerTurnIsUnsettled(binding)) {
|
|
37
|
+
return {
|
|
38
|
+
action: "attention",
|
|
39
|
+
conversationId: conversation.conversationId,
|
|
40
|
+
reason: "Provider Conversation is missing but input delivery remains unsettled."
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (currentProviderActivation(binding) !== null || binding.authority.owner !== "none") {
|
|
44
|
+
return {
|
|
45
|
+
action: "attention",
|
|
46
|
+
conversationId: conversation.conversationId,
|
|
47
|
+
reason: "Provider Conversation is missing but its Activation writer has not ended."
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return { action: "replace", conversationId: conversation.conversationId };
|
|
51
|
+
}
|
|
52
|
+
function providerTurnIsUnsettled(binding) {
|
|
53
|
+
return binding.turn !== null
|
|
54
|
+
&& ["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn.status);
|
|
55
|
+
}
|