@zq-silk/yui 0.8.1 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/ARCHITECTURE.md +27 -28
  2. package/README.md +57 -46
  3. package/dist/cli/commandCatalog.js +57 -17
  4. package/dist/cli/interactionPolicy.js +4 -10
  5. package/dist/cli/invocationRouter.js +2 -1
  6. package/dist/cli.js +106 -27
  7. package/dist/commands/taskCommands.js +458 -77
  8. package/dist/commands/taskCompletionGate.js +152 -0
  9. package/dist/context/runContextPack.js +19 -4
  10. package/dist/context/sessionBootstrapManifest.js +1 -1
  11. package/dist/controller/fileSchedulerStoreAdapter.js +389 -70
  12. package/dist/controller/resourceInventory.js +9 -5
  13. package/dist/controller/runtime.js +80 -7
  14. package/dist/controller/runtimeLaunchCoordinator.js +18 -78
  15. package/dist/controller/structuredProviderObservation.js +273 -0
  16. package/dist/executor/agentAdapter.js +40 -0
  17. package/dist/executor/agentExecutor.js +31 -7
  18. package/dist/executor/executorRegistry.js +11 -49
  19. package/dist/executor/fileRoleLaunchPlanner.js +115 -37
  20. package/dist/lifecycle/canonicalLifecycleEvent.js +5 -2
  21. package/dist/repository/gitWorkspace.js +7 -4
  22. package/dist/repository/taskBaseFreshness.js +4 -2
  23. package/dist/run/agentRun.js +4 -4
  24. package/dist/runtime/agentHost.js +767 -158
  25. package/dist/runtime/builtinAgentDrivers.js +1 -5
  26. package/dist/runtime/codexAppServerRuntime.js +67 -60
  27. package/dist/runtime/exactControlPlane.js +7 -2
  28. package/dist/runtime/index.js +6 -2
  29. package/dist/runtime/launchBroker.js +30 -8
  30. package/dist/runtime/providerAuthorityFence.js +24 -0
  31. package/dist/runtime/providerControl.js +63 -0
  32. package/dist/runtime/providerRecoveryDecision.js +55 -0
  33. package/dist/runtime/providerRuntimeIdentity.js +269 -19
  34. package/dist/runtime/runtimeBinding.js +20 -11
  35. package/dist/runtime/structuredProviderHost.js +476 -0
  36. package/dist/runtime/tmuxAdapters.js +143 -42
  37. package/dist/scheduler/activeRoleRunDelivery.js +206 -120
  38. package/dist/scheduler/leaderWakeupProcessor.js +141 -16
  39. package/dist/scheduler/wakeReason.js +1 -0
  40. package/dist/storage/migration/productionRegistry.js +111 -0
  41. package/dist/storage/sqliteStore.js +2 -0
  42. package/dist/storage/taskStore.js +3 -1
  43. package/dist/task/completionReadiness.js +43 -0
  44. package/dist/task/nextAction.js +6 -4
  45. package/dist/task/publicationReference.js +1 -0
  46. package/dist/tmux/tmuxManager.js +1 -1
  47. package/dist/workItem/workItem.js +12 -0
  48. package/dist/workspace/workItemChangeSetManager.js +2 -1
  49. package/i18n/README.zh-CN.md +11 -8
  50. package/package.json +1 -1
  51. package/skills/yui-leader/SKILL.md +8 -3
  52. package/skills/yui-runtime/SKILL.md +7 -2
@@ -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 snapshot = await this.readConversation(input.conversationId);
51
- if (input.expectedNoActiveTurn && snapshot.activeTurnId !== undefined) {
52
- return { status: "not-accepted", reason: `active-turn:${snapshot.activeTurnId}` };
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: snapshot.threadId,
94
+ threadId,
57
95
  ...(input.clientUserMessageId === undefined
58
96
  ? {}
59
97
  : { clientUserMessageId: text(input.clientUserMessageId, "Codex input attempt id") }),
60
- input: [{ type: "text", text: text(input.text, "Codex Turn 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: [{ type: "text", text: text(input.text, "Codex steer 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.nativeSessionId !== current.nativeSessionId) {
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({
@@ -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 { TmuxPromptPushAdapter, TmuxSessionHost } from "./tmuxAdapters.js";
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.providerInput !== undefined) {
79
- if (payload.providerInput.kind !== "stdin-json-user-message") {
80
- throw new Error("Agent Host Provider input transport is invalid.");
81
- }
82
- if (typeof payload.providerInput.boundedText !== "string"
83
- || payload.providerInput.boundedText.includes("\0")
84
- || Buffer.byteLength(payload.providerInput.boundedText, "utf8") > 4 * 1024) {
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
+ }