@zq-silk/yui 0.13.4 → 0.13.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/ARCHITECTURE.md +13 -13
- package/README.md +19 -20
- package/dist/cli/commandCatalog.js +22 -21
- package/dist/cli/interactionPolicy.js +0 -14
- package/dist/cli.js +42 -0
- package/dist/commands/executionAuditCommands.js +1 -1
- package/dist/commands/taskCommands.js +60 -326
- package/dist/commands/taskContextCommand.js +3 -14
- package/dist/commands/taskExecutionCommands.js +254 -0
- package/dist/commands/taskNextActionCommand.js +1 -3
- package/dist/commands/taskOverviewCommand.js +9 -2
- package/dist/commands/taskRoleRuntimeStatus.js +2 -25
- package/dist/controller/agentRuntimeObserver.js +4 -2
- package/dist/controller/clientRuntime.js +45 -2
- package/dist/controller/controller.js +6 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +59 -188
- package/dist/controller/jobControl.js +3 -2
- package/dist/controller/runtime.js +6 -33
- package/dist/controller/runtimeEventProcessor.js +8 -4
- package/dist/controller/runtimeHookRunFence.js +4 -10
- package/dist/execution/executionHealth.js +8 -16
- package/dist/executor/agentAdapter.js +3 -8
- package/dist/executor/agentExecutor.js +13 -14
- package/dist/executor/fileRoleLaunchPlanner.js +10 -26
- package/dist/lifecycle/exactRunTerminalization.js +24 -322
- package/dist/repository/taskWorkspaceCoordinator.js +0 -9
- package/dist/runtime/agentHost.js +22 -83
- package/dist/runtime/builtinAgentDrivers.js +1 -1
- package/dist/runtime/exactControlPlane.js +15 -9
- package/dist/runtime/launchBroker.js +1 -11
- package/dist/runtime/providerContinuationReconciliationService.js +1 -1
- package/dist/runtime/providerRecoveryDecision.js +1 -1
- package/dist/runtime/providerRuntimeIdentity.js +25 -15
- package/dist/runtime/structuredProviderHost.js +0 -57
- package/dist/scheduler/activeRoleRunDelivery.js +1 -19
- package/dist/scheduler/leaderWakeupProcessor.js +12 -63
- package/dist/scheduler/ports.js +3 -2
- package/dist/scheduler/roleRunLiveness.js +4 -1
- package/dist/scheduler/roleRunStall.js +0 -2
- package/dist/scheduler/taskExecutionProjection.js +18 -1
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/storage/migration/productionRegistry.js +65 -0
- package/dist/storage/sqliteStore.js +10 -2
- package/dist/storage/taskStore.js +11 -3
- package/dist/task/completionReadiness.js +0 -67
- package/dist/task/nextAction.js +16 -32
- package/dist/task/task.js +38 -3
- package/dist/web/assets/client/i18n.js +0 -4
- package/dist/web/assets/client/view.js +0 -18
- package/dist/web/webSnapshot.js +7 -13
- package/i18n/README.zh-CN.md +4 -4
- package/package.json +1 -1
- package/dist/run/recoveryProjection.js +0 -252
- package/dist/runtime/conversationSwitch.js +0 -277
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
|
|
3
|
-
import { actionableExecutionLaneRecoveries } from "../execution/executionHealth.js";
|
|
4
|
-
import { runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
|
|
5
|
-
import { runHasActiveRuntimeOperations } from "../runtime/runtimeObservation.js";
|
|
6
|
-
export const RUN_RECOVERY_ACTIONS = [
|
|
7
|
-
"diagnose",
|
|
8
|
-
"retry",
|
|
9
|
-
"terminate"
|
|
10
|
-
];
|
|
11
|
-
/**
|
|
12
|
-
* Reads every durable record the recovery projection needs. Returns null
|
|
13
|
-
* only when the Run itself is absent.
|
|
14
|
-
*/
|
|
15
|
-
export function readRunRecoveryFacts(store, taskId, runId) {
|
|
16
|
-
const run = store.getAgentRun(taskId, runId);
|
|
17
|
-
if (run === null || run.taskId !== taskId)
|
|
18
|
-
return null;
|
|
19
|
-
const task = store.getTask(taskId);
|
|
20
|
-
const sessionSet = store.getTaskRoleSessionSet(taskId, run.roleName);
|
|
21
|
-
const events = store.listEvents(taskId);
|
|
22
|
-
const roleMailbox = store.getWorkMailbox?.({
|
|
23
|
-
kind: "role",
|
|
24
|
-
taskId,
|
|
25
|
-
roleName: run.roleName
|
|
26
|
-
}) ?? null;
|
|
27
|
-
const progress = latestRunDurableProgressAt(store, taskId, run.roleName, runId);
|
|
28
|
-
return {
|
|
29
|
-
run,
|
|
30
|
-
task: task === null ? null : { id: task.id, status: task.status },
|
|
31
|
-
sessionSet,
|
|
32
|
-
inputDeliveryUnsettled: roleMailbox?.inputDelivery != null,
|
|
33
|
-
blockingProviderContinuation: runOwnsBlockingProviderContinuation(events, {
|
|
34
|
-
taskId,
|
|
35
|
-
roleName: run.roleName,
|
|
36
|
-
runId: run.id,
|
|
37
|
-
agentId: run.effective.agentId
|
|
38
|
-
}),
|
|
39
|
-
activeRuntimeOperation: runHasActiveRuntimeOperations(events, {
|
|
40
|
-
taskId,
|
|
41
|
-
roleName: run.roleName,
|
|
42
|
-
runId: run.id,
|
|
43
|
-
agentId: run.effective.agentId
|
|
44
|
-
}),
|
|
45
|
-
progress,
|
|
46
|
-
latestProviderObservation: latestRunProviderObservation(events, runId)
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Resolve the exact live-Run recovery plans referenced by Lane health. Failed
|
|
51
|
-
* terminal Lanes use `task run retry` directly and therefore need no live-Run
|
|
52
|
-
* recovery projection here.
|
|
53
|
-
*/
|
|
54
|
-
export function projectExecutionLaneRunRecoveries(store, taskId, groups) {
|
|
55
|
-
const runIds = new Set(actionableExecutionLaneRecoveries(groups).flatMap((lane) => (lane.runId === undefined || lane.recovery === "retry-new-agent-run"
|
|
56
|
-
? []
|
|
57
|
-
: [lane.runId])));
|
|
58
|
-
return [...runIds].flatMap((runId) => {
|
|
59
|
-
const facts = readRunRecoveryFacts(store, taskId, runId);
|
|
60
|
-
return facts === null ? [] : [projectRunRecovery(facts)];
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Latest Provider observation for a Run. Provider timestamps are evidence:
|
|
65
|
-
* they explain why a stale fence was supplied but never authorize recovery.
|
|
66
|
-
*/
|
|
67
|
-
function latestRunProviderObservation(events, runId) {
|
|
68
|
-
let latest = null;
|
|
69
|
-
for (const event of events) {
|
|
70
|
-
if (event.type !== "runtime.observation")
|
|
71
|
-
continue;
|
|
72
|
-
if (event.payload.runId !== runId)
|
|
73
|
-
continue;
|
|
74
|
-
const kind = typeof event.payload.kind === "string" ? event.payload.kind : "unknown";
|
|
75
|
-
const receivedAt = typeof event.payload.receivedAt === "string"
|
|
76
|
-
&& Number.isFinite(Date.parse(event.payload.receivedAt))
|
|
77
|
-
? event.payload.receivedAt
|
|
78
|
-
: event.createdAt;
|
|
79
|
-
const at = Date.parse(receivedAt);
|
|
80
|
-
if (latest === null || at > latest.at) {
|
|
81
|
-
latest = { kind, receivedAt, at };
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return latest === null ? null : { kind: latest.kind, receivedAt: latest.receivedAt };
|
|
85
|
-
}
|
|
86
|
-
export function projectRunRecovery(facts) {
|
|
87
|
-
const { run, task, sessionSet, progress } = facts;
|
|
88
|
-
const session = activeSession(facts);
|
|
89
|
-
const canonicalProgressAt = progress?.progressAt ?? null;
|
|
90
|
-
const accepted = run.deliveredAt !== undefined;
|
|
91
|
-
const acceptanceOptions = accepted
|
|
92
|
-
? ["accepted", "ambiguous"]
|
|
93
|
-
: ["rejected", "ambiguous"];
|
|
94
|
-
const blocked = recoveryBlocker(facts, session, canonicalProgressAt);
|
|
95
|
-
const sessionTerminal = session?.status === "stopped" || session?.status === "broken";
|
|
96
|
-
const providerTerminal = sessionSet?.providerBinding?.turn?.status === "failed"
|
|
97
|
-
|| sessionSet?.providerBinding?.turn?.status === "cancelled"
|
|
98
|
-
|| sessionSet?.providerBinding?.turn?.status === "rejected";
|
|
99
|
-
const supportedActions = RUN_RECOVERY_ACTIONS.filter((action) => ((action !== "retry" || !sessionTerminal)
|
|
100
|
-
&& (action !== "terminate" || sessionTerminal || providerTerminal)));
|
|
101
|
-
const actions = blocked === null
|
|
102
|
-
? supportedActions.map((action) => buildActionPlan(facts, action, session, canonicalProgressAt))
|
|
103
|
-
: [];
|
|
104
|
-
const judgmentRequired = blocked === null && actions.some((plan) => plan.argv.includes(PROVIDER_ACCEPTANCE_PLACEHOLDER))
|
|
105
|
-
? "Provider acceptance is not durably determined for every action; pass --provider-acceptance explicitly."
|
|
106
|
-
: undefined;
|
|
107
|
-
return {
|
|
108
|
-
taskId: run.taskId,
|
|
109
|
-
runId: run.id,
|
|
110
|
-
roleName: run.roleName,
|
|
111
|
-
runStatus: run.status,
|
|
112
|
-
recoverable: blocked === null,
|
|
113
|
-
canonicalProgressAt,
|
|
114
|
-
...(progress?.evidence === undefined ? {} : { canonicalProgressEvidence: progress.evidence }),
|
|
115
|
-
provider: {
|
|
116
|
-
acceptedAt: run.deliveredAt ?? null,
|
|
117
|
-
observedAt: facts.latestProviderObservation?.receivedAt ?? null,
|
|
118
|
-
observationKind: facts.latestProviderObservation?.kind ?? null
|
|
119
|
-
},
|
|
120
|
-
providerAcceptance: {
|
|
121
|
-
accepted,
|
|
122
|
-
options: acceptanceOptions
|
|
123
|
-
},
|
|
124
|
-
session: session === null ? null : {
|
|
125
|
-
status: session.status,
|
|
126
|
-
...(session.nativeSessionId === undefined ? {} : { nativeSessionId: session.nativeSessionId }),
|
|
127
|
-
...(session.launchId === undefined ? {} : { launchId: session.launchId })
|
|
128
|
-
},
|
|
129
|
-
actions,
|
|
130
|
-
...(judgmentRequired === undefined ? {} : { judgmentRequired }),
|
|
131
|
-
...(blocked === null ? {} : { reason: blocked })
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
const PROVIDER_ACCEPTANCE_PLACEHOLDER = "<accepted|rejected|ambiguous>";
|
|
135
|
-
function activeSession(facts) {
|
|
136
|
-
const sessions = facts.sessionSet;
|
|
137
|
-
if (sessions === null)
|
|
138
|
-
return null;
|
|
139
|
-
const session = sessions.sessions[sessions.activeAgentId];
|
|
140
|
-
if (session === undefined)
|
|
141
|
-
return null;
|
|
142
|
-
if (session.agentId !== facts.run.effective.agentId)
|
|
143
|
-
return null;
|
|
144
|
-
if (session.adapterId !== facts.run.effective.adapterId)
|
|
145
|
-
return null;
|
|
146
|
-
return session;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Mirrors the fail-closed checks of `recoverExactAgentRun` that are visible
|
|
150
|
-
* from durable records. A non-null result means recovery cannot currently be
|
|
151
|
-
* applied; the canonical fence is still projected for diagnosis.
|
|
152
|
-
*/
|
|
153
|
-
function recoveryBlocker(facts, session, canonicalProgressAt) {
|
|
154
|
-
const { run, task } = facts;
|
|
155
|
-
if (task === null)
|
|
156
|
-
return "task-missing";
|
|
157
|
-
if (task.status !== "active")
|
|
158
|
-
return "task-terminal";
|
|
159
|
-
if (run.status !== "active")
|
|
160
|
-
return "run-terminal";
|
|
161
|
-
if (canonicalProgressAt === null)
|
|
162
|
-
return "progress-unavailable";
|
|
163
|
-
if (session === null)
|
|
164
|
-
return "session-missing";
|
|
165
|
-
if (facts.inputDeliveryUnsettled)
|
|
166
|
-
return "provider-input-delivery-unsettled";
|
|
167
|
-
if (facts.blockingProviderContinuation)
|
|
168
|
-
return "provider-continuation-writer-owned";
|
|
169
|
-
if (facts.activeRuntimeOperation)
|
|
170
|
-
return "provider-operation-active";
|
|
171
|
-
const binding = facts.sessionSet?.providerBinding;
|
|
172
|
-
if (run.deliveredAt !== undefined
|
|
173
|
-
&& (binding === null || binding?.turn === null))
|
|
174
|
-
return "provider-turn-state-missing";
|
|
175
|
-
if (binding !== null && binding !== undefined) {
|
|
176
|
-
if (["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn?.status ?? ""))
|
|
177
|
-
return "provider-turn-unsettled";
|
|
178
|
-
if (binding.authority.owner === "human" || binding.authority.owner === "unknown") {
|
|
179
|
-
return "provider-writer-authority-unavailable";
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
function buildActionPlan(facts, action, session, canonicalProgressAt) {
|
|
185
|
-
const { run } = facts;
|
|
186
|
-
const acceptance = actionAcceptance(facts, action);
|
|
187
|
-
const argv = [
|
|
188
|
-
"task",
|
|
189
|
-
"run",
|
|
190
|
-
"recover",
|
|
191
|
-
`${run.taskId}/${run.id}`,
|
|
192
|
-
"--action",
|
|
193
|
-
action,
|
|
194
|
-
"--expected-progress-at",
|
|
195
|
-
canonicalProgressAt,
|
|
196
|
-
"--provider-acceptance",
|
|
197
|
-
acceptance,
|
|
198
|
-
"--reason",
|
|
199
|
-
"<text>",
|
|
200
|
-
"--agent-id",
|
|
201
|
-
run.effective.agentId,
|
|
202
|
-
"--adapter-id",
|
|
203
|
-
run.effective.adapterId,
|
|
204
|
-
...(session.nativeSessionId === undefined
|
|
205
|
-
? []
|
|
206
|
-
: ["--native-session-id", session.nativeSessionId]),
|
|
207
|
-
...(session.launchId === undefined
|
|
208
|
-
? []
|
|
209
|
-
: ["--launch-id", session.launchId])
|
|
210
|
-
];
|
|
211
|
-
const command = `yui ${argv
|
|
212
|
-
.map((part) => (part === "<text>" ? '"<text>"' : part))
|
|
213
|
-
.join(" ")}`;
|
|
214
|
-
const fingerprintSource = [
|
|
215
|
-
run.id,
|
|
216
|
-
action,
|
|
217
|
-
canonicalProgressAt,
|
|
218
|
-
run.effective.agentId,
|
|
219
|
-
run.effective.adapterId,
|
|
220
|
-
session.nativeSessionId ?? "",
|
|
221
|
-
session.launchId ?? ""
|
|
222
|
-
].join("|");
|
|
223
|
-
return {
|
|
224
|
-
action,
|
|
225
|
-
reason: ACTION_REASONS[action],
|
|
226
|
-
expectedProgressAt: canonicalProgressAt,
|
|
227
|
-
agentId: run.effective.agentId,
|
|
228
|
-
adapterId: run.effective.adapterId,
|
|
229
|
-
...(session.nativeSessionId === undefined
|
|
230
|
-
? {}
|
|
231
|
-
: { nativeSessionId: session.nativeSessionId }),
|
|
232
|
-
...(session.launchId === undefined ? {} : { launchId: session.launchId }),
|
|
233
|
-
command,
|
|
234
|
-
argv,
|
|
235
|
-
fingerprint: createHash("sha256").update(fingerprintSource).digest("hex")
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* The acceptance value for the copy-paste command. When exactly one value is
|
|
240
|
-
* durably valid it is filled in (the durable record, not a guess); otherwise
|
|
241
|
-
* the Leader must choose and the command carries an explicit placeholder.
|
|
242
|
-
*/
|
|
243
|
-
function actionAcceptance(facts, action) {
|
|
244
|
-
if (action === "diagnose")
|
|
245
|
-
return PROVIDER_ACCEPTANCE_PLACEHOLDER;
|
|
246
|
-
return facts.run.deliveredAt === undefined ? "rejected" : "accepted";
|
|
247
|
-
}
|
|
248
|
-
const ACTION_REASONS = {
|
|
249
|
-
diagnose: "Collect bounded diagnostics before any state-changing recovery.",
|
|
250
|
-
retry: "Request another provider turn on the same native Session when the failure is transient.",
|
|
251
|
-
terminate: "Fail the Run explicitly when recovery is not viable."
|
|
252
|
-
};
|
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
import { roleAgentSessionResumeMode } from "../executor/agentExecutor.js";
|
|
2
|
-
import { effectiveLaunchSnapshotsCompatibleForTaskSession } from "../executor/effectiveLaunch.js";
|
|
3
|
-
import { currentProviderActivation, currentProviderConversation } from "./providerRuntimeIdentity.js";
|
|
4
|
-
import { blockingProviderContinuations } from "./runtimeContinuationProjection.js";
|
|
5
|
-
import { runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
|
|
6
|
-
export const CONVERSATION_SWITCH_REQUESTED_EVENT = "runtime.conversation-switch-requested";
|
|
7
|
-
export const CONVERSATION_SWITCH_RESOLVED_EVENT = "runtime.conversation-switch-resolved";
|
|
8
|
-
export const CONVERSATION_SWITCH_DETACHED_EVENT = "runtime.conversation-switch-detached";
|
|
9
|
-
export function providerConversationGeneration(sessions) {
|
|
10
|
-
const binding = sessions?.providerBinding;
|
|
11
|
-
if (binding === null || binding === undefined)
|
|
12
|
-
return null;
|
|
13
|
-
const current = currentProviderConversation(binding);
|
|
14
|
-
return `${binding.providerNamespace}:${binding.accountScope}:${current.epoch}:${current.conversationId}`;
|
|
15
|
-
}
|
|
16
|
-
export function projectConversationSwitch(events, roleName, sessions) {
|
|
17
|
-
const requests = events.filter((event) => (event.type === CONVERSATION_SWITCH_REQUESTED_EVENT
|
|
18
|
-
&& event.payload.roleName === roleName));
|
|
19
|
-
const request = requests.at(-1);
|
|
20
|
-
if (request === undefined)
|
|
21
|
-
return null;
|
|
22
|
-
const requestId = request.payload.requestId;
|
|
23
|
-
const generation = request.payload.generation;
|
|
24
|
-
const requestedBy = request.payload.requestedBy;
|
|
25
|
-
const reason = request.payload.reason;
|
|
26
|
-
if (requestId === undefined || generation === undefined || reason === undefined
|
|
27
|
-
|| (requestedBy !== "user" && requestedBy !== "operator" && requestedBy !== "leader")) {
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
const resolution = [...events].reverse().find((event) => (event.type === CONVERSATION_SWITCH_RESOLVED_EVENT
|
|
31
|
-
&& event.payload.requestId === requestId));
|
|
32
|
-
const explicitStatus = resolution?.payload.status;
|
|
33
|
-
const currentGeneration = providerConversationGeneration(sessions);
|
|
34
|
-
const status = explicitStatus === "applied" || explicitStatus === "obsolete"
|
|
35
|
-
? explicitStatus
|
|
36
|
-
: currentGeneration !== null && currentGeneration !== generation
|
|
37
|
-
? "obsolete"
|
|
38
|
-
: "pending";
|
|
39
|
-
return {
|
|
40
|
-
requestId,
|
|
41
|
-
roleName,
|
|
42
|
-
generation,
|
|
43
|
-
requestedBy,
|
|
44
|
-
reason,
|
|
45
|
-
requestedAt: request.createdAt,
|
|
46
|
-
status,
|
|
47
|
-
...(resolution === undefined ? {} : { resolvedAt: resolution.createdAt })
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
export function pendingConversationSwitch(events, roleName, sessions) {
|
|
51
|
-
const projected = projectConversationSwitch(events, roleName, sessions);
|
|
52
|
-
return projected?.status === "pending" ? projected : null;
|
|
53
|
-
}
|
|
54
|
-
export function roleSessionDispatchModeWithConversationSwitch(sessions, events, mailbox, roleName, agentId, effective) {
|
|
55
|
-
const ordinary = roleAgentSessionResumeMode(sessions, agentId, effective);
|
|
56
|
-
if (ordinary !== "resume") {
|
|
57
|
-
if (sessions?.providerBinding !== null && sessions?.providerBinding !== undefined) {
|
|
58
|
-
if (freshConversationLaunchAllowed({ sessions, events, mailbox, roleName })) {
|
|
59
|
-
return "new";
|
|
60
|
-
}
|
|
61
|
-
const existing = sessions.sessions[agentId];
|
|
62
|
-
if (existing?.nativeSessionId !== undefined
|
|
63
|
-
&& (existing.status === "stopped" || existing.status === "broken")
|
|
64
|
-
&& effectiveLaunchSnapshotsCompatibleForTaskSession(existing.effective, effective)) {
|
|
65
|
-
// A terminal local Activation does not prove the Provider Conversation
|
|
66
|
-
// is gone. Reattach to the same native identity by default.
|
|
67
|
-
return "resume";
|
|
68
|
-
}
|
|
69
|
-
throw new Error(`Fresh Provider Conversation is not yet safe: ${sessions.owner.taskId}/${roleName}.`);
|
|
70
|
-
}
|
|
71
|
-
return ordinary;
|
|
72
|
-
}
|
|
73
|
-
if (sessions?.providerBinding !== null && sessions?.providerBinding !== undefined
|
|
74
|
-
&& freshConversationLaunchAllowed({ sessions, events, mailbox, roleName })) {
|
|
75
|
-
return "new";
|
|
76
|
-
}
|
|
77
|
-
return "resume";
|
|
78
|
-
}
|
|
79
|
-
export function conversationDetachmentBasis(input) {
|
|
80
|
-
const { sessions, mailbox } = input;
|
|
81
|
-
const binding = sessions.providerBinding;
|
|
82
|
-
const activation = binding === null ? null : currentProviderActivation(binding);
|
|
83
|
-
if (input.runMode !== "new" || binding === null || activation === null
|
|
84
|
-
|| sessions.inFlight?.runId !== input.runId
|
|
85
|
-
|| mailbox?.processing?.batchId !== sessions.inFlight.receiptId
|
|
86
|
-
|| mailbox.processing.owner !== "controller"
|
|
87
|
-
|| mailbox.processing.executionRef?.type !== "run"
|
|
88
|
-
|| mailbox.processing.executionRef.taskId !== sessions.owner.taskId
|
|
89
|
-
|| mailbox.processing.executionRef.id !== input.runId
|
|
90
|
-
|| !actorRequestedSwitchBoundaryReady(sessions, mailbox)
|
|
91
|
-
|| currentConversationExecutionBlockers(sessions, input.events, input.roleName).length > 0
|
|
92
|
-
|| pendingConversationSwitch(input.events, input.roleName, sessions) === null) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
return "actor-request";
|
|
96
|
-
}
|
|
97
|
-
export function conversationReplacementBasis(input) {
|
|
98
|
-
const { sessions, mailbox } = input;
|
|
99
|
-
const binding = sessions.providerBinding;
|
|
100
|
-
if (input.runMode !== "new" || binding === null
|
|
101
|
-
|| sessions.inFlight?.runId !== input.runId
|
|
102
|
-
|| mailbox?.processing?.batchId !== sessions.inFlight.receiptId
|
|
103
|
-
|| mailbox.processing.owner !== "controller"
|
|
104
|
-
|| mailbox?.processing?.executionRef?.type !== "run"
|
|
105
|
-
|| mailbox.processing.executionRef.taskId !== sessions.owner.taskId
|
|
106
|
-
|| mailbox.processing.executionRef.id !== input.runId
|
|
107
|
-
|| mailbox.inputDelivery !== null
|
|
108
|
-
|| !conversationIsQuiescent(sessions, mailbox)
|
|
109
|
-
|| currentConversationExecutionBlockers(sessions, input.events, input.roleName).length > 0) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
if (currentConversationIsExactlyUnrecoverable(sessions, input.events, input.roleName))
|
|
113
|
-
return "exact-unrecoverable";
|
|
114
|
-
return pendingConversationSwitch(input.events, input.roleName, sessions) === null
|
|
115
|
-
? null
|
|
116
|
-
: "actor-request";
|
|
117
|
-
}
|
|
118
|
-
export function freshConversationLaunchAllowed(input) {
|
|
119
|
-
return freshConversationLaunchBlockers(input).length === 0;
|
|
120
|
-
}
|
|
121
|
-
/** Exact reasons a fresh Conversation cannot currently be admitted. */
|
|
122
|
-
export function freshConversationLaunchBlockers(input) {
|
|
123
|
-
const { sessions } = input;
|
|
124
|
-
if (sessions === null)
|
|
125
|
-
return [];
|
|
126
|
-
const session = sessions.sessions[sessions.activeAgentId];
|
|
127
|
-
if (sessions.providerBinding === null) {
|
|
128
|
-
return session === undefined
|
|
129
|
-
|| session.status === "stopped"
|
|
130
|
-
|| session.status === "broken"
|
|
131
|
-
? []
|
|
132
|
-
: ["native-session-not-terminal"];
|
|
133
|
-
}
|
|
134
|
-
const request = pendingConversationSwitch(input.events, input.roleName, sessions);
|
|
135
|
-
const binding = sessions.providerBinding;
|
|
136
|
-
const blockers = [];
|
|
137
|
-
if (input.mailbox?.inputDelivery != null)
|
|
138
|
-
blockers.push("provider-input-delivery-unsettled");
|
|
139
|
-
if (binding.turn !== null
|
|
140
|
-
&& ["submitting", "accepted", "running", "delivery-unknown"].includes(binding.turn.status))
|
|
141
|
-
blockers.push("provider-turn-unsettled");
|
|
142
|
-
blockers.push(...currentConversationExecutionBlockers(sessions, input.events, input.roleName));
|
|
143
|
-
if (blockers.length > 0)
|
|
144
|
-
return blockers;
|
|
145
|
-
if (request !== null && actorRequestedSwitchBoundaryReady(sessions, input.mailbox, input.candidateRunId))
|
|
146
|
-
return [];
|
|
147
|
-
if (currentProviderActivation(binding) !== null)
|
|
148
|
-
blockers.push("provider-activation-active");
|
|
149
|
-
if (binding.authority.owner !== "none")
|
|
150
|
-
blockers.push("provider-writer-authority-owned");
|
|
151
|
-
if (blockers.length > 0)
|
|
152
|
-
return blockers;
|
|
153
|
-
if (currentProviderConversation(binding).recoverability === "unrecoverable") {
|
|
154
|
-
return currentConversationIsExactlyUnrecoverable(sessions, input.events, input.roleName) ? [] : ["exact-unrecoverable-evidence-missing"];
|
|
155
|
-
}
|
|
156
|
-
if (request !== null)
|
|
157
|
-
return ["actor-switch-boundary-not-ready"];
|
|
158
|
-
return ["current-conversation-recoverable"];
|
|
159
|
-
}
|
|
160
|
-
function actorRequestedSwitchBoundaryReady(sessions, mailbox, candidateRunId) {
|
|
161
|
-
const binding = sessions?.providerBinding;
|
|
162
|
-
if (sessions === null || sessions === undefined
|
|
163
|
-
|| binding === null || binding === undefined
|
|
164
|
-
|| mailbox?.inputDelivery != null)
|
|
165
|
-
return false;
|
|
166
|
-
const turnSettled = binding.turn === null
|
|
167
|
-
|| ["completed", "failed", "cancelled", "rejected"].includes(binding.turn.status);
|
|
168
|
-
if (!turnSettled)
|
|
169
|
-
return false;
|
|
170
|
-
const session = sessions.sessions[sessions.activeAgentId];
|
|
171
|
-
if (session?.status === "running")
|
|
172
|
-
return false;
|
|
173
|
-
const activation = currentProviderActivation(binding);
|
|
174
|
-
if (activation === null)
|
|
175
|
-
return binding.authority.owner === "none";
|
|
176
|
-
if (binding.authority.owner !== "controller"
|
|
177
|
-
|| binding.authority.holderId !== activation.activationId)
|
|
178
|
-
return false;
|
|
179
|
-
if (session === undefined)
|
|
180
|
-
return false;
|
|
181
|
-
if (sessions.inFlight === null) {
|
|
182
|
-
if (mailbox?.processing === null)
|
|
183
|
-
return true;
|
|
184
|
-
return candidateRunId !== undefined
|
|
185
|
-
&& mailbox?.processing?.owner === "controller"
|
|
186
|
-
&& mailbox.processing.executionRef?.type === "run"
|
|
187
|
-
&& mailbox.processing.executionRef.taskId === sessions.owner.taskId
|
|
188
|
-
&& mailbox.processing.executionRef.id === candidateRunId;
|
|
189
|
-
}
|
|
190
|
-
return mailbox?.processing?.batchId === sessions.inFlight.receiptId
|
|
191
|
-
&& mailbox.processing.owner === "controller"
|
|
192
|
-
&& mailbox.processing.executionRef?.type === "run"
|
|
193
|
-
&& mailbox.processing.executionRef.taskId === sessions.owner.taskId
|
|
194
|
-
&& mailbox.processing.executionRef.id === sessions.inFlight.runId;
|
|
195
|
-
}
|
|
196
|
-
function conversationIsQuiescent(sessions, mailbox) {
|
|
197
|
-
const binding = sessions?.providerBinding;
|
|
198
|
-
if (binding === null || binding === undefined)
|
|
199
|
-
return false;
|
|
200
|
-
const turnSettled = binding.turn === null
|
|
201
|
-
|| ["completed", "failed", "cancelled", "rejected"].includes(binding.turn.status);
|
|
202
|
-
return turnSettled
|
|
203
|
-
&& currentProviderActivation(binding) === null
|
|
204
|
-
&& binding.authority.owner === "none"
|
|
205
|
-
&& mailbox?.inputDelivery == null;
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* A projected recoverability flag is necessary but not sufficient to replace
|
|
209
|
-
* a Conversation. Require the exact structured missing-Conversation fact for
|
|
210
|
-
* the current Provider identity and latest Activation generation, so an old
|
|
211
|
-
* observation cannot authorize a later fresh Conversation. The binding's Run
|
|
212
|
-
* id is deliberately excluded: replacement work rebinds that fence before
|
|
213
|
-
* launching, while the missing fact necessarily came from the preceding Run's
|
|
214
|
-
* exact attempt to resume this same Activation.
|
|
215
|
-
*/
|
|
216
|
-
function currentConversationIsExactlyUnrecoverable(sessions, events, roleName) {
|
|
217
|
-
const binding = sessions.providerBinding;
|
|
218
|
-
if (binding === null)
|
|
219
|
-
return false;
|
|
220
|
-
const conversation = currentProviderConversation(binding);
|
|
221
|
-
if (conversation.recoverability !== "unrecoverable")
|
|
222
|
-
return false;
|
|
223
|
-
const activation = [...binding.activations].reverse().find((entry) => (entry.conversationId === conversation.conversationId));
|
|
224
|
-
const generationStartedAt = activation?.startedAt ?? conversation.createdAt;
|
|
225
|
-
return events.some((event) => {
|
|
226
|
-
const observation = runtimeObservationFromTaskEvent(event);
|
|
227
|
-
if (observation === null
|
|
228
|
-
|| observation.kind !== "conversation.observed"
|
|
229
|
-
|| observation.payload.recoverability !== "unrecoverable"
|
|
230
|
-
|| (observation.authority !== "provider-structured"
|
|
231
|
-
&& observation.authority !== "controller"))
|
|
232
|
-
return false;
|
|
233
|
-
const fence = observation.fence;
|
|
234
|
-
if (fence.taskId !== sessions.owner.taskId
|
|
235
|
-
|| fence.roleName !== roleName
|
|
236
|
-
|| fence.agentId !== binding.accountScope
|
|
237
|
-
|| fence.driverId !== binding.providerNamespace
|
|
238
|
-
|| fence.conversationId !== conversation.conversationId
|
|
239
|
-
|| (activation !== undefined && fence.activationId !== activation.activationId)) {
|
|
240
|
-
return false;
|
|
241
|
-
}
|
|
242
|
-
return Date.parse(observation.observedAt ?? observation.receivedAt)
|
|
243
|
-
>= Date.parse(generationStartedAt);
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
/** Current Provider-owned work that may outlive a foreground Turn. */
|
|
247
|
-
function currentConversationExecutionBlockers(sessions, events, roleName) {
|
|
248
|
-
const binding = sessions.providerBinding;
|
|
249
|
-
if (binding === null)
|
|
250
|
-
return [];
|
|
251
|
-
const conversation = currentProviderConversation(binding);
|
|
252
|
-
const activation = [...binding.activations].reverse().find((entry) => (entry.conversationId === conversation.conversationId));
|
|
253
|
-
if (activation === undefined)
|
|
254
|
-
return ["provider-activation-identity-missing"];
|
|
255
|
-
const activeOperation = events.some((event) => {
|
|
256
|
-
const observation = runtimeObservationFromTaskEvent(event);
|
|
257
|
-
if (observation?.kind !== "operation.started")
|
|
258
|
-
return false;
|
|
259
|
-
const fence = observation.fence;
|
|
260
|
-
return fence.taskId === sessions.owner.taskId
|
|
261
|
-
&& fence.roleName === roleName
|
|
262
|
-
&& fence.agentId === binding.accountScope
|
|
263
|
-
&& fence.driverId === binding.providerNamespace
|
|
264
|
-
&& (fence.conversationId ?? fence.nativeSessionId) === conversation.conversationId
|
|
265
|
-
&& (fence.activationId ?? fence.launchId) === activation.activationId;
|
|
266
|
-
});
|
|
267
|
-
const blockingContinuation = blockingProviderContinuations(events).some((entry) => (entry.taskId === sessions.owner.taskId
|
|
268
|
-
&& entry.roleName === roleName
|
|
269
|
-
&& entry.identity.providerNamespace === binding.providerNamespace
|
|
270
|
-
&& entry.identity.accountScope === binding.accountScope
|
|
271
|
-
&& entry.identity.conversationId === conversation.conversationId
|
|
272
|
-
&& entry.identity.activationId === activation.activationId));
|
|
273
|
-
return [
|
|
274
|
-
...(activeOperation ? ["provider-operation-active"] : []),
|
|
275
|
-
...(blockingContinuation ? ["provider-continuation-writer-owned"] : [])
|
|
276
|
-
];
|
|
277
|
-
}
|