@zq-silk/yui 0.6.8 → 0.6.10
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 +10 -3
- package/dist/cli/commandCatalog.js +1 -1
- package/dist/commands/taskCommands.js +63 -28
- package/dist/commands/taskContextCommand.js +27 -1
- package/dist/commands/taskRoleRuntimeStatus.js +17 -6
- package/dist/controller/agentRuntimeObserver.js +6 -3
- package/dist/controller/controller.js +29 -47
- package/dist/controller/fileSchedulerStoreAdapter.js +572 -258
- package/dist/controller/runtime.js +8 -1
- package/dist/controller/runtimeEventInbox.js +16 -5
- package/dist/controller/runtimeHookRunFence.js +51 -5
- package/dist/controller/runtimeObservationHook.js +8 -2
- package/dist/coordination/workMailbox.js +408 -28
- package/dist/coordination/workMailboxQueue.js +12 -10
- package/dist/executor/agentExecutor.js +101 -94
- package/dist/executor/executorRegistry.js +47 -2
- package/dist/executor/fileRoleLaunchPlanner.js +4 -2
- package/dist/lifecycle/exactRunTerminalization.js +1 -7
- package/dist/repository/taskWorkspaceCoordinator.js +9 -4
- package/dist/runtime/agentDriver.js +83 -4
- package/dist/runtime/agentDriverObservation.js +25 -10
- package/dist/runtime/builtinAgentDrivers.js +168 -18
- package/dist/runtime/codexAppServerRuntime.js +355 -0
- package/dist/runtime/continuationManager.js +117 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/lifecycleReservation.js +4 -3
- package/dist/runtime/promptEnvelope.js +14 -3
- package/dist/runtime/providerContinuation.js +225 -0
- package/dist/runtime/providerContinuationReconciliationService.js +172 -0
- package/dist/runtime/providerRuntimeIdentity.js +232 -0
- package/dist/runtime/providerRuntimeReconciler.js +166 -0
- package/dist/runtime/runtimeContinuationProjection.js +34 -0
- package/dist/runtime/runtimeObservation.js +217 -6
- package/dist/runtime/runtimeProjection.js +172 -11
- package/dist/scheduler/activeRoleRunDelivery.js +314 -1
- package/dist/scheduler/leaderWakeupProcessor.js +2 -1
- package/dist/scheduler/operatorInputNotificationProcessor.js +3 -2
- package/dist/scheduler/roleRunLiveness.js +8 -7
- package/dist/scheduler/roleRunStall.js +4 -2
- package/dist/scheduler/taskExecutionProjection.js +2 -2
- package/dist/storage/migration/productionRegistry.js +474 -1
- package/dist/storage/sqliteSchema.js +102 -21
- package/dist/storage/sqliteStore.js +52 -110
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +40 -53
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -21
- package/dist/task/nextAction.js +1 -1
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +233 -4
- package/dist/web/assets/client/i18n.js +166 -2
- package/dist/web/assets/client/view.js +30 -13
- package/dist/web/assets/styles/cards.js +62 -0
- package/dist/web/assets/styles/widgets.js +1 -0
- package/dist/web/webSnapshot.js +11 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +33 -24
- package/skills/yui-operator/SKILL.md +7 -5
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
export function createProviderRuntimeBinding(input) {
|
|
2
|
+
const startedAt = timestamp(input.startedAt, "Provider Activation startedAt");
|
|
3
|
+
return validateProviderRuntimeBinding({
|
|
4
|
+
schemaVersion: 1,
|
|
5
|
+
providerNamespace: identity(input.providerNamespace, "Provider namespace"),
|
|
6
|
+
accountScope: identity(input.accountScope, "Provider account scope"),
|
|
7
|
+
runId: identity(input.runId, "Run id"),
|
|
8
|
+
currentConversationEpoch: 1,
|
|
9
|
+
conversations: [{
|
|
10
|
+
conversationId: identity(input.conversationId, "Provider Conversation id"),
|
|
11
|
+
epoch: 1,
|
|
12
|
+
status: "current",
|
|
13
|
+
recoverability: "unknown",
|
|
14
|
+
createdAt: startedAt
|
|
15
|
+
}],
|
|
16
|
+
activations: [{
|
|
17
|
+
activationId: identity(input.activationId, "Provider Activation id"),
|
|
18
|
+
conversationId: input.conversationId,
|
|
19
|
+
generation: 1,
|
|
20
|
+
status: "active",
|
|
21
|
+
writerLease: true,
|
|
22
|
+
startedAt
|
|
23
|
+
}]
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export function currentProviderConversation(binding) {
|
|
27
|
+
validateProviderRuntimeBinding(binding);
|
|
28
|
+
return binding.conversations.find((entry) => (entry.epoch === binding.currentConversationEpoch && entry.status === "current"));
|
|
29
|
+
}
|
|
30
|
+
export function currentProviderActivation(binding) {
|
|
31
|
+
const conversation = currentProviderConversation(binding);
|
|
32
|
+
return [...binding.activations].reverse().find((entry) => (entry.conversationId === conversation.conversationId && entry.status === "active")) ?? null;
|
|
33
|
+
}
|
|
34
|
+
export function startProviderActivation(raw, input) {
|
|
35
|
+
const binding = validateProviderRuntimeBinding(raw);
|
|
36
|
+
if (currentProviderActivation(binding) !== null) {
|
|
37
|
+
throw new Error("Provider Conversation already has a live writer Activation.");
|
|
38
|
+
}
|
|
39
|
+
const conversation = currentProviderConversation(binding);
|
|
40
|
+
const generation = binding.activations
|
|
41
|
+
.filter((entry) => entry.conversationId === conversation.conversationId)
|
|
42
|
+
.reduce((maximum, entry) => Math.max(maximum, entry.generation), 0) + 1;
|
|
43
|
+
return validateProviderRuntimeBinding({
|
|
44
|
+
...binding,
|
|
45
|
+
activations: [...binding.activations, {
|
|
46
|
+
activationId: identity(input.activationId, "Provider Activation id"),
|
|
47
|
+
conversationId: conversation.conversationId,
|
|
48
|
+
generation,
|
|
49
|
+
status: "active",
|
|
50
|
+
writerLease: true,
|
|
51
|
+
startedAt: timestamp(input.startedAt, "Provider Activation startedAt")
|
|
52
|
+
}]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
export function endProviderActivation(raw, activationId, input) {
|
|
56
|
+
const binding = validateProviderRuntimeBinding(raw);
|
|
57
|
+
const id = identity(activationId, "Provider Activation id");
|
|
58
|
+
const target = binding.activations.find((entry) => entry.activationId === id);
|
|
59
|
+
if (target === undefined)
|
|
60
|
+
throw new Error(`Provider Activation is not recorded: ${id}.`);
|
|
61
|
+
if (target.status !== "active")
|
|
62
|
+
return binding;
|
|
63
|
+
const endedAt = timestamp(input.endedAt, "Provider Activation endedAt");
|
|
64
|
+
if (Date.parse(endedAt) < Date.parse(target.startedAt)) {
|
|
65
|
+
throw new Error("Provider Activation endedAt is earlier than startedAt.");
|
|
66
|
+
}
|
|
67
|
+
return validateProviderRuntimeBinding({
|
|
68
|
+
...binding,
|
|
69
|
+
activations: binding.activations.map((entry) => entry.activationId === id
|
|
70
|
+
? {
|
|
71
|
+
...entry,
|
|
72
|
+
status: input.status,
|
|
73
|
+
writerLease: false,
|
|
74
|
+
endedAt,
|
|
75
|
+
...(input.reason === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: { terminalReason: identity(input.reason, "Provider Activation terminal reason") })
|
|
78
|
+
}
|
|
79
|
+
: entry)
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
export function updateProviderConversationRecoverability(raw, recoverability) {
|
|
83
|
+
const binding = validateProviderRuntimeBinding(raw);
|
|
84
|
+
const current = currentProviderConversation(binding);
|
|
85
|
+
return validateProviderRuntimeBinding({
|
|
86
|
+
...binding,
|
|
87
|
+
conversations: binding.conversations.map((entry) => entry.epoch === current.epoch
|
|
88
|
+
? { ...entry, recoverability }
|
|
89
|
+
: entry)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export function supersedeProviderConversation(raw, input) {
|
|
93
|
+
const binding = validateProviderRuntimeBinding(raw);
|
|
94
|
+
const current = currentProviderConversation(binding);
|
|
95
|
+
if (current.recoverability !== "unrecoverable") {
|
|
96
|
+
throw new Error("Current Provider Conversation is not exactly unrecoverable.");
|
|
97
|
+
}
|
|
98
|
+
if (!input.noUnsettledInputDelivery) {
|
|
99
|
+
throw new Error("Cannot replace a Provider Conversation with unsettled input delivery.");
|
|
100
|
+
}
|
|
101
|
+
if (!input.writerUmbrellaClear || currentProviderActivation(binding) !== null) {
|
|
102
|
+
throw new Error("Cannot replace a Provider Conversation while its writer umbrella is owned.");
|
|
103
|
+
}
|
|
104
|
+
const switchedAt = timestamp(input.switchedAt, "Provider Conversation switch timestamp");
|
|
105
|
+
const epoch = current.epoch + 1;
|
|
106
|
+
return validateProviderRuntimeBinding({
|
|
107
|
+
...binding,
|
|
108
|
+
currentConversationEpoch: epoch,
|
|
109
|
+
conversations: [
|
|
110
|
+
...binding.conversations.map((entry) => entry.epoch === current.epoch
|
|
111
|
+
? { ...entry, status: "superseded", supersededAt: switchedAt }
|
|
112
|
+
: entry),
|
|
113
|
+
{
|
|
114
|
+
conversationId: identity(input.conversationId, "Provider Conversation id"),
|
|
115
|
+
epoch,
|
|
116
|
+
status: "current",
|
|
117
|
+
recoverability: "unknown",
|
|
118
|
+
createdAt: switchedAt
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
activations: [...binding.activations, {
|
|
122
|
+
activationId: identity(input.activationId, "Provider Activation id"),
|
|
123
|
+
conversationId: input.conversationId,
|
|
124
|
+
generation: 1,
|
|
125
|
+
status: "active",
|
|
126
|
+
writerLease: true,
|
|
127
|
+
startedAt: switchedAt
|
|
128
|
+
}]
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export function validateProviderRuntimeBinding(value) {
|
|
132
|
+
if (value.schemaVersion !== 1)
|
|
133
|
+
throw new Error("Provider Runtime Binding schemaVersion must be 1.");
|
|
134
|
+
identity(value.providerNamespace, "Provider namespace");
|
|
135
|
+
identity(value.accountScope, "Provider account scope");
|
|
136
|
+
identity(value.runId, "Run id");
|
|
137
|
+
integer(value.currentConversationEpoch, 1, "Current Provider Conversation epoch");
|
|
138
|
+
if (!Array.isArray(value.conversations) || value.conversations.length === 0) {
|
|
139
|
+
throw new Error("Provider Runtime Binding requires a Conversation.");
|
|
140
|
+
}
|
|
141
|
+
const conversationIds = new Set();
|
|
142
|
+
const epochs = new Set();
|
|
143
|
+
let currentCount = 0;
|
|
144
|
+
for (const conversation of value.conversations) {
|
|
145
|
+
identity(conversation.conversationId, "Provider Conversation id");
|
|
146
|
+
integer(conversation.epoch, 1, "Provider Conversation epoch");
|
|
147
|
+
if (conversationIds.has(conversation.conversationId) || epochs.has(conversation.epoch)) {
|
|
148
|
+
throw new Error("Provider Runtime Binding contains duplicate Conversation identity.");
|
|
149
|
+
}
|
|
150
|
+
conversationIds.add(conversation.conversationId);
|
|
151
|
+
epochs.add(conversation.epoch);
|
|
152
|
+
if (conversation.status !== "current" && conversation.status !== "superseded") {
|
|
153
|
+
throw new Error("Provider Conversation status is invalid.");
|
|
154
|
+
}
|
|
155
|
+
if (!["unknown", "recoverable", "unrecoverable"].includes(conversation.recoverability)) {
|
|
156
|
+
throw new Error("Provider Conversation recoverability is invalid.");
|
|
157
|
+
}
|
|
158
|
+
timestamp(conversation.createdAt, "Provider Conversation createdAt");
|
|
159
|
+
if (conversation.status === "current") {
|
|
160
|
+
currentCount += 1;
|
|
161
|
+
if (conversation.epoch !== value.currentConversationEpoch) {
|
|
162
|
+
throw new Error("Current Provider Conversation epoch is inconsistent.");
|
|
163
|
+
}
|
|
164
|
+
if (conversation.supersededAt !== undefined) {
|
|
165
|
+
throw new Error("Current Provider Conversation cannot be superseded.");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (conversation.supersededAt === undefined) {
|
|
169
|
+
throw new Error("Superseded Provider Conversation requires supersededAt.");
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
timestamp(conversation.supersededAt, "Provider Conversation supersededAt");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (currentCount !== 1)
|
|
176
|
+
throw new Error("Provider Runtime Binding requires one current Conversation.");
|
|
177
|
+
const activationIds = new Set();
|
|
178
|
+
const activeByConversation = new Set();
|
|
179
|
+
const generations = new Set();
|
|
180
|
+
for (const activation of value.activations) {
|
|
181
|
+
identity(activation.activationId, "Provider Activation id");
|
|
182
|
+
if (activationIds.has(activation.activationId)) {
|
|
183
|
+
throw new Error("Provider Runtime Binding contains duplicate Activation identity.");
|
|
184
|
+
}
|
|
185
|
+
activationIds.add(activation.activationId);
|
|
186
|
+
if (!conversationIds.has(activation.conversationId)) {
|
|
187
|
+
throw new Error("Provider Activation references an unknown Conversation.");
|
|
188
|
+
}
|
|
189
|
+
integer(activation.generation, 1, "Provider Activation generation");
|
|
190
|
+
const generationKey = `${activation.conversationId}\u0000${activation.generation}`;
|
|
191
|
+
if (generations.has(generationKey)) {
|
|
192
|
+
throw new Error("Provider Runtime Binding contains duplicate Activation generation.");
|
|
193
|
+
}
|
|
194
|
+
generations.add(generationKey);
|
|
195
|
+
if (!["active", "ended", "failed"].includes(activation.status)) {
|
|
196
|
+
throw new Error("Provider Activation status is invalid.");
|
|
197
|
+
}
|
|
198
|
+
timestamp(activation.startedAt, "Provider Activation startedAt");
|
|
199
|
+
if (activation.status === "active") {
|
|
200
|
+
if (!activation.writerLease || activation.endedAt !== undefined) {
|
|
201
|
+
throw new Error("Active Provider Activation must hold its writer lease.");
|
|
202
|
+
}
|
|
203
|
+
if (activeByConversation.has(activation.conversationId)) {
|
|
204
|
+
throw new Error("Provider Conversation has multiple live writer Activations.");
|
|
205
|
+
}
|
|
206
|
+
activeByConversation.add(activation.conversationId);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
if (activation.writerLease || activation.endedAt === undefined) {
|
|
210
|
+
throw new Error("Terminal Provider Activation must release its writer lease.");
|
|
211
|
+
}
|
|
212
|
+
timestamp(activation.endedAt, "Provider Activation endedAt");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return value;
|
|
216
|
+
}
|
|
217
|
+
function identity(value, label) {
|
|
218
|
+
if (typeof value !== "string" || value.trim().length === 0)
|
|
219
|
+
throw new Error(`${label} is invalid.`);
|
|
220
|
+
return value.trim();
|
|
221
|
+
}
|
|
222
|
+
function timestamp(value, label) {
|
|
223
|
+
const normalized = identity(value, label);
|
|
224
|
+
if (!Number.isFinite(Date.parse(normalized)))
|
|
225
|
+
throw new Error(`${label} must be a timestamp.`);
|
|
226
|
+
return normalized;
|
|
227
|
+
}
|
|
228
|
+
function integer(value, minimum, label) {
|
|
229
|
+
if (!Number.isSafeInteger(value) || value < minimum)
|
|
230
|
+
throw new Error(`${label} is invalid.`);
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { continuationOwnsWriterUmbrella, observeProviderContinuation, providerContinuationKey } from "./providerContinuation.js";
|
|
2
|
+
const BASE_RECONCILE_MS = 2_000;
|
|
3
|
+
const MAX_RECONCILE_MS = 5 * 60_000;
|
|
4
|
+
const CIRCUIT_ERROR_LIMIT = 5;
|
|
5
|
+
const QUERY_TIMEOUT_MS = 5_000;
|
|
6
|
+
/**
|
|
7
|
+
* Reconciles only already-known children, grouped under one Conversation and
|
|
8
|
+
* Activation generation. Missing entries settle ownership only for an exact
|
|
9
|
+
* snapshot; partial/unavailable absence is never terminal evidence.
|
|
10
|
+
*/
|
|
11
|
+
export async function reconcileKnownDetachedContinuations(input) {
|
|
12
|
+
const candidates = input.continuations.filter((entry) => (entry.attachment === "detached" && continuationOwnsWriterUmbrella(entry)));
|
|
13
|
+
if (candidates.length === 0) {
|
|
14
|
+
return { continuations: input.continuations, schedule: null, quality: "exact", changed: false };
|
|
15
|
+
}
|
|
16
|
+
const first = candidates[0];
|
|
17
|
+
const groupKey = [
|
|
18
|
+
first.identity.providerNamespace,
|
|
19
|
+
first.identity.accountScope,
|
|
20
|
+
first.identity.conversationId,
|
|
21
|
+
first.identity.activationId
|
|
22
|
+
].join("\u0000");
|
|
23
|
+
if (candidates.some((entry) => [
|
|
24
|
+
entry.identity.providerNamespace,
|
|
25
|
+
entry.identity.accountScope,
|
|
26
|
+
entry.identity.conversationId,
|
|
27
|
+
entry.identity.activationId
|
|
28
|
+
].join("\u0000") !== groupKey)) {
|
|
29
|
+
throw new Error("Provider reconcile input must contain one Conversation/Activation group.");
|
|
30
|
+
}
|
|
31
|
+
const nowMs = input.now.getTime();
|
|
32
|
+
if (input.previous?.circuitOpenUntil !== undefined
|
|
33
|
+
&& Date.parse(input.previous.circuitOpenUntil) > nowMs) {
|
|
34
|
+
return {
|
|
35
|
+
continuations: input.continuations,
|
|
36
|
+
schedule: input.previous,
|
|
37
|
+
quality: "unavailable",
|
|
38
|
+
changed: false
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
let result;
|
|
42
|
+
try {
|
|
43
|
+
const raw = await withTimeout(input.port.queryKnownContinuations({
|
|
44
|
+
providerNamespace: first.identity.providerNamespace,
|
|
45
|
+
accountScope: first.identity.accountScope,
|
|
46
|
+
conversationId: first.identity.conversationId,
|
|
47
|
+
activationId: first.identity.activationId,
|
|
48
|
+
continuations: Object.freeze(candidates.map((entry) => Object.freeze({
|
|
49
|
+
continuationId: entry.identity.continuationId,
|
|
50
|
+
generation: entry.identity.generation
|
|
51
|
+
})))
|
|
52
|
+
}), QUERY_TIMEOUT_MS);
|
|
53
|
+
result = validateQueryResult(raw, new Set(candidates.map((entry) => (providerContinuationKey(entry.identity)))));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
const errors = (input.previous?.consecutiveErrors ?? 0) + 1;
|
|
57
|
+
const delay = backoff(input.previous?.attempts ?? 0);
|
|
58
|
+
const circuitOpenUntil = errors >= CIRCUIT_ERROR_LIMIT
|
|
59
|
+
? new Date(nowMs + MAX_RECONCILE_MS).toISOString()
|
|
60
|
+
: undefined;
|
|
61
|
+
return {
|
|
62
|
+
continuations: input.continuations,
|
|
63
|
+
schedule: {
|
|
64
|
+
key: groupKey,
|
|
65
|
+
attempts: (input.previous?.attempts ?? 0) + 1,
|
|
66
|
+
consecutiveErrors: errors,
|
|
67
|
+
nextReconcileAt: new Date(nowMs + delay).toISOString(),
|
|
68
|
+
...(circuitOpenUntil === undefined ? {} : { circuitOpenUntil })
|
|
69
|
+
},
|
|
70
|
+
quality: "unavailable",
|
|
71
|
+
changed: false
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const observations = new Map(result.continuations.map((entry) => [entry.key, entry]));
|
|
75
|
+
let changed = false;
|
|
76
|
+
const updated = input.continuations.map((continuation) => {
|
|
77
|
+
const observed = observations.get(providerContinuationKey(continuation.identity));
|
|
78
|
+
if (observed === undefined) {
|
|
79
|
+
// Exact absence closes a known child mechanically, without inventing a
|
|
80
|
+
// successful outcome or ResultRef. Partial absence preserves ownership.
|
|
81
|
+
if (result.quality !== "exact")
|
|
82
|
+
return continuation;
|
|
83
|
+
const next = observeProviderContinuation(continuation, {
|
|
84
|
+
execution: "quiescent",
|
|
85
|
+
outcome: "unknown",
|
|
86
|
+
attachment: "detached",
|
|
87
|
+
observation: "exact",
|
|
88
|
+
mayWriteWorkspace: false,
|
|
89
|
+
observedAt: input.now.toISOString()
|
|
90
|
+
});
|
|
91
|
+
changed ||= next !== continuation;
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
const next = observeProviderContinuation(continuation, {
|
|
95
|
+
execution: observed.execution,
|
|
96
|
+
outcome: observed.outcome,
|
|
97
|
+
attachment: "detached",
|
|
98
|
+
observation: result.quality,
|
|
99
|
+
mayWriteWorkspace: observed.mayWriteWorkspace,
|
|
100
|
+
observedAt: input.now.toISOString(),
|
|
101
|
+
...(observed.resultRef === undefined ? {} : { resultRef: observed.resultRef }),
|
|
102
|
+
...(observed.providerSequence === undefined
|
|
103
|
+
? {}
|
|
104
|
+
: { providerSequence: observed.providerSequence })
|
|
105
|
+
});
|
|
106
|
+
changed ||= next !== continuation;
|
|
107
|
+
return next;
|
|
108
|
+
});
|
|
109
|
+
const unsettled = updated.some((entry) => (entry.attachment === "detached" && continuationOwnsWriterUmbrella(entry)));
|
|
110
|
+
return {
|
|
111
|
+
continuations: Object.freeze(updated),
|
|
112
|
+
schedule: unsettled
|
|
113
|
+
? {
|
|
114
|
+
key: groupKey,
|
|
115
|
+
attempts: (input.previous?.attempts ?? 0) + 1,
|
|
116
|
+
consecutiveErrors: 0,
|
|
117
|
+
nextReconcileAt: new Date(nowMs + backoff(input.previous?.attempts ?? 0)).toISOString()
|
|
118
|
+
}
|
|
119
|
+
: null,
|
|
120
|
+
quality: result.quality,
|
|
121
|
+
changed
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function validateQueryResult(input, expectedKeys) {
|
|
125
|
+
if (input === null || typeof input !== "object"
|
|
126
|
+
|| !["exact", "partial", "unavailable"].includes(input.quality)
|
|
127
|
+
|| !Array.isArray(input.continuations)) {
|
|
128
|
+
throw new Error("Provider continuation metadata response is invalid.");
|
|
129
|
+
}
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
for (const entry of input.continuations) {
|
|
132
|
+
if (entry === null || typeof entry !== "object"
|
|
133
|
+
|| typeof entry.key !== "string" || !expectedKeys.has(entry.key)
|
|
134
|
+
|| seen.has(entry.key)
|
|
135
|
+
|| !["active", "quiescent", "unknown"].includes(entry.execution)
|
|
136
|
+
|| !["pending", "succeeded", "failed", "cancelled", "unknown"].includes(entry.outcome)
|
|
137
|
+
|| typeof entry.mayWriteWorkspace !== "boolean"
|
|
138
|
+
|| (entry.execution === "active" && entry.outcome !== "pending")
|
|
139
|
+
|| (entry.execution === "quiescent" && entry.outcome === "pending")
|
|
140
|
+
|| (entry.providerSequence !== undefined
|
|
141
|
+
&& (!Number.isSafeInteger(entry.providerSequence) || entry.providerSequence < 0))) {
|
|
142
|
+
throw new Error("Provider continuation metadata entry is invalid or conflicting.");
|
|
143
|
+
}
|
|
144
|
+
seen.add(entry.key);
|
|
145
|
+
}
|
|
146
|
+
return input;
|
|
147
|
+
}
|
|
148
|
+
async function withTimeout(promise, timeoutMs) {
|
|
149
|
+
let timer;
|
|
150
|
+
try {
|
|
151
|
+
return await Promise.race([
|
|
152
|
+
promise,
|
|
153
|
+
new Promise((_resolve, reject) => {
|
|
154
|
+
timer = setTimeout(() => reject(new Error("Provider continuation metadata query timed out.")), timeoutMs);
|
|
155
|
+
timer.unref?.();
|
|
156
|
+
})
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
if (timer !== undefined)
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function backoff(attempts) {
|
|
165
|
+
return Math.min(MAX_RECONCILE_MS, BASE_RECONCILE_MS * (2 ** Math.min(attempts, 8)));
|
|
166
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { foldContinuationObservation } from "./continuationManager.js";
|
|
2
|
+
import { continuationOwnsWriterUmbrella, providerContinuationKey } from "./providerContinuation.js";
|
|
3
|
+
import { runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
|
|
4
|
+
export function projectProviderContinuations(events) {
|
|
5
|
+
const projected = new Map();
|
|
6
|
+
const observations = events
|
|
7
|
+
.map(runtimeObservationFromTaskEvent)
|
|
8
|
+
.filter((entry) => (entry !== null && entry.kind.startsWith("continuation.")));
|
|
9
|
+
// Fold in durable Task-event order. Provider sequence is an identity-local
|
|
10
|
+
// monotonic fence applied by ProviderContinuation; it is not a global clock.
|
|
11
|
+
// In particular, Controller-derived exact snapshots intentionally have no
|
|
12
|
+
// Provider sequence and must not be moved before the Provider start fact.
|
|
13
|
+
for (const observation of observations) {
|
|
14
|
+
const fence = observation.fence;
|
|
15
|
+
if (fence.conversationId === undefined || fence.activationId === undefined
|
|
16
|
+
|| fence.continuationId === undefined || fence.continuationGeneration === undefined) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const key = [
|
|
20
|
+
fence.driverId,
|
|
21
|
+
fence.agentId,
|
|
22
|
+
fence.conversationId,
|
|
23
|
+
fence.activationId,
|
|
24
|
+
fence.continuationId,
|
|
25
|
+
fence.continuationGeneration
|
|
26
|
+
].join("\u0000");
|
|
27
|
+
const result = foldContinuationObservation(projected.get(key) ?? null, observation);
|
|
28
|
+
projected.set(providerContinuationKey(result.continuation.identity), result.continuation);
|
|
29
|
+
}
|
|
30
|
+
return Object.freeze([...projected.values()].sort((left, right) => (providerContinuationKey(left.identity).localeCompare(providerContinuationKey(right.identity)))));
|
|
31
|
+
}
|
|
32
|
+
export function blockingProviderContinuations(events) {
|
|
33
|
+
return projectProviderContinuations(events).filter((entry) => (continuationOwnsWriterUmbrella(entry) || entry.identityConflict));
|
|
34
|
+
}
|