@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,117 @@
|
|
|
1
|
+
import { createProviderContinuation, observeProviderContinuation, providerContinuationKey, recordProviderReport } from "./providerContinuation.js";
|
|
2
|
+
import { createRuntimeObservation } from "./runtimeObservation.js";
|
|
3
|
+
export function foldContinuationObservation(existing, raw) {
|
|
4
|
+
const observation = createRuntimeObservation(raw);
|
|
5
|
+
if (!observation.kind.startsWith("continuation.")) {
|
|
6
|
+
throw new Error("ContinuationManager requires a continuation observation.");
|
|
7
|
+
}
|
|
8
|
+
const identity = continuationIdentity(observation);
|
|
9
|
+
const payload = observation.payload;
|
|
10
|
+
const base = existing ?? createProviderContinuation({
|
|
11
|
+
taskId: observation.fence.taskId,
|
|
12
|
+
roleName: observation.fence.roleName,
|
|
13
|
+
runId: observation.fence.runId,
|
|
14
|
+
identity,
|
|
15
|
+
...(observation.fence.parentContinuationId === undefined
|
|
16
|
+
? {}
|
|
17
|
+
: { parentContinuationId: observation.fence.parentContinuationId }),
|
|
18
|
+
attachment: payload.attachment,
|
|
19
|
+
observation: payload.observationQuality,
|
|
20
|
+
// A report is model-visible information, not settlement evidence. When
|
|
21
|
+
// it arrives before start, keep a conservative writer umbrella until an
|
|
22
|
+
// exact continuation.settled or complete native-work snapshot releases it.
|
|
23
|
+
mayWriteWorkspace: observation.kind === "continuation.reported"
|
|
24
|
+
? true
|
|
25
|
+
: payload.mayWriteWorkspace,
|
|
26
|
+
observedAt: observation.observedAt ?? observation.receivedAt,
|
|
27
|
+
...(observation.sequence === undefined ? {} : { providerSequence: observation.sequence })
|
|
28
|
+
});
|
|
29
|
+
if (existing !== null && providerContinuationKey(existing.identity) !== providerContinuationKey(identity)) {
|
|
30
|
+
throw new Error("Continuation observation identity does not match the existing projection.");
|
|
31
|
+
}
|
|
32
|
+
if (observation.kind === "continuation.reported") {
|
|
33
|
+
const next = recordProviderReport(base, {
|
|
34
|
+
reportId: payload.reportId,
|
|
35
|
+
...(payload.resultRef === undefined ? {} : { resultRef: payload.resultRef }),
|
|
36
|
+
...(payload.providerDeliveryRef === undefined
|
|
37
|
+
? {}
|
|
38
|
+
: { providerDeliveryRef: payload.providerDeliveryRef }),
|
|
39
|
+
observedAt: observation.observedAt ?? observation.receivedAt
|
|
40
|
+
}, observation.sequence);
|
|
41
|
+
return {
|
|
42
|
+
disposition: next === base ? "duplicate" : "reported",
|
|
43
|
+
continuation: next
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const next = observeProviderContinuation(base, {
|
|
47
|
+
execution: payload.execution,
|
|
48
|
+
outcome: payload.outcome,
|
|
49
|
+
attachment: payload.attachment,
|
|
50
|
+
observation: payload.observationQuality,
|
|
51
|
+
mayWriteWorkspace: payload.mayWriteWorkspace,
|
|
52
|
+
observedAt: observation.observedAt ?? observation.receivedAt,
|
|
53
|
+
...(payload.resultRef === undefined ? {} : { resultRef: payload.resultRef }),
|
|
54
|
+
...(observation.sequence === undefined ? {} : { providerSequence: observation.sequence })
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
disposition: next === base
|
|
58
|
+
? existing === null ? "created" : "duplicate"
|
|
59
|
+
: next.identityConflict ? "conflict"
|
|
60
|
+
: observation.kind === "continuation.settled" ? "settled"
|
|
61
|
+
: existing === null ? "created" : "updated",
|
|
62
|
+
continuation: next
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Commits the semantic continuation fact and its Leader notification before
|
|
67
|
+
* releasing child ownership. The surrounding TaskStore transaction is the
|
|
68
|
+
* crash boundary; a replay with the same semanticKey becomes a no-op.
|
|
69
|
+
*/
|
|
70
|
+
export function applyContinuationObservationAtomically(store, raw, factRef) {
|
|
71
|
+
const observation = createRuntimeObservation(raw);
|
|
72
|
+
const identity = continuationIdentity(observation);
|
|
73
|
+
return store.transaction((tx) => {
|
|
74
|
+
const result = foldContinuationObservation(tx.getProviderContinuation(providerContinuationKey(identity)), observation);
|
|
75
|
+
if (!tx.appendContinuationFactOnce(observation.semanticKey, observation, result.continuation)) {
|
|
76
|
+
return { disposition: "duplicate", continuation: result.continuation };
|
|
77
|
+
}
|
|
78
|
+
tx.saveProviderContinuation(result.continuation);
|
|
79
|
+
if (result.disposition === "reported" || result.disposition === "settled"
|
|
80
|
+
|| result.disposition === "conflict") {
|
|
81
|
+
tx.enqueueContinuationSignal({
|
|
82
|
+
taskId: result.continuation.taskId,
|
|
83
|
+
roleName: "leader",
|
|
84
|
+
signal: {
|
|
85
|
+
reason: result.disposition === "reported"
|
|
86
|
+
? "provider-continuation-report"
|
|
87
|
+
: result.disposition === "settled"
|
|
88
|
+
? "provider-continuation-settled"
|
|
89
|
+
: "provider-continuation-identity-conflict",
|
|
90
|
+
refs: [factRef],
|
|
91
|
+
occurredAt: observation.observedAt ?? observation.receivedAt,
|
|
92
|
+
source: "continuation-manager",
|
|
93
|
+
dedupeKey: observation.semanticKey,
|
|
94
|
+
deliveryMode: "followup",
|
|
95
|
+
lane: "normal"
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function continuationIdentity(observation) {
|
|
103
|
+
const fence = observation.fence;
|
|
104
|
+
if (fence.taskId === undefined || fence.runId === undefined
|
|
105
|
+
|| fence.conversationId === undefined || fence.activationId === undefined
|
|
106
|
+
|| fence.continuationId === undefined) {
|
|
107
|
+
throw new Error("Continuation observation fence is incomplete.");
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
providerNamespace: fence.driverId,
|
|
111
|
+
accountScope: fence.agentId,
|
|
112
|
+
conversationId: fence.conversationId,
|
|
113
|
+
activationId: fence.activationId,
|
|
114
|
+
continuationId: fence.continuationId,
|
|
115
|
+
generation: fence.continuationGeneration
|
|
116
|
+
};
|
|
117
|
+
}
|
package/dist/runtime/index.js
CHANGED
|
@@ -9,4 +9,6 @@ export { FileTaskRuntimeIsolation, YUI_TASK_RUNTIME_ISOLATION_DESCRIPTOR, YUI_TA
|
|
|
9
9
|
export { createSessionOwnerIdentity, discoverProviderRootByLaunchEnv, isLinuxProcessLive, listLaunchFencedProcesses, listOwnedProcessTree, readLinuxProcessIdentity } from "./sessionOwnerIdentity.js";
|
|
10
10
|
export { FileSessionOwnerRegistry } from "./sessionOwnerRegistry.js";
|
|
11
11
|
export { DEFAULT_FORCED_GRACE_MS, DEFAULT_GRACEFUL_GRACE_MS, terminateSessionOwners } from "./sessionTerminationGuard.js";
|
|
12
|
+
export { ProviderContinuationReconciliationService } from "./providerContinuationReconciliationService.js";
|
|
13
|
+
export { codexNotificationBoundary, CodexAppServerRequestError, CodexAppServerRuntime } from "./codexAppServerRuntime.js";
|
|
12
14
|
export { reconcileSessionOwners } from "./sessionReconciliation.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mailboxHasWork } from "../coordination/workMailbox.js";
|
|
1
2
|
export const RUNTIME_LIFECYCLE_OWNER = "runtime-lifecycle";
|
|
2
3
|
export const RUNTIME_LAUNCH_RESERVED_REASON = "runtime-launch-reserved";
|
|
3
4
|
export const RUNTIME_CLEANUP_REQUIRED_REASON = "runtime-cleanup-required";
|
|
@@ -28,11 +29,11 @@ export function hasRuntimeLaunchReservation(mailbox) {
|
|
|
28
29
|
return isRuntimeLaunchReservation(mailbox?.processing);
|
|
29
30
|
}
|
|
30
31
|
export function hasRuntimeCleanupObligation(mailbox) {
|
|
31
|
-
|
|
32
|
+
const pending = mailbox?.pending.normal;
|
|
33
|
+
return pending?.reasons.includes(RUNTIME_CLEANUP_REQUIRED_REASON) === true
|
|
32
34
|
|| (!isRuntimeLaunchReservation(mailbox?.processing)
|
|
33
35
|
&& mailbox?.processing?.batch.reasons.includes(RUNTIME_CLEANUP_REQUIRED_REASON) === true);
|
|
34
36
|
}
|
|
35
37
|
export function hasRuntimeLifecycleWork(mailbox) {
|
|
36
|
-
return mailbox !== null
|
|
37
|
-
&& (mailbox.processing !== null || mailbox.pending !== null);
|
|
38
|
+
return mailbox !== null && mailboxHasWork(mailbox);
|
|
38
39
|
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { requireText, requireTimestamp } from "./validation.js";
|
|
2
2
|
import { formatAgentRunReceiptId, formatInputRequestReceiptId, validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
3
|
export function createPromptEnvelope(input) {
|
|
4
|
-
if (input.source.kind !== "agent-run"
|
|
4
|
+
if (input.source.kind !== "agent-run"
|
|
5
|
+
&& input.source.kind !== "run-input"
|
|
6
|
+
&& input.source.kind !== "input-request") {
|
|
5
7
|
throw new Error("Prompt source kind is invalid.");
|
|
6
8
|
}
|
|
7
9
|
const source = validateTaskRecordReference({
|
|
8
10
|
taskId: input.source.taskId,
|
|
9
11
|
localId: input.source.localId
|
|
10
|
-
}, input.source.kind === "
|
|
12
|
+
}, input.source.kind === "input-request" ? "inputRequest" : "agentRun");
|
|
11
13
|
const id = requireQualifiedReceiptId(input.id, input.source.kind, source.taskId, source.localId);
|
|
12
14
|
return {
|
|
13
15
|
id,
|
|
@@ -23,7 +25,16 @@ export function createPromptEnvelope(input) {
|
|
|
23
25
|
function requireQualifiedReceiptId(value, kind, taskId, localId) {
|
|
24
26
|
const expected = kind === "agent-run"
|
|
25
27
|
? formatAgentRunReceiptId(taskId, localId)
|
|
26
|
-
:
|
|
28
|
+
: kind === "input-request"
|
|
29
|
+
? formatInputRequestReceiptId(taskId, localId)
|
|
30
|
+
: `agent-input:${taskId}/${localId}/`;
|
|
31
|
+
if (kind === "run-input") {
|
|
32
|
+
if (!value.startsWith(expected)
|
|
33
|
+
|| !/^(normal|user-correction):[1-9]\d*-[1-9]\d*$/.test(value.slice(expected.length))) {
|
|
34
|
+
throw new Error("Prompt envelope id does not match its source.");
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
27
38
|
if (value !== expected)
|
|
28
39
|
throw new Error("Prompt envelope id does not match its source.");
|
|
29
40
|
return expected;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
export function providerContinuationKey(identity) {
|
|
2
|
+
const normalized = validateProviderContinuationIdentity(identity);
|
|
3
|
+
return [
|
|
4
|
+
normalized.providerNamespace,
|
|
5
|
+
normalized.accountScope,
|
|
6
|
+
normalized.conversationId,
|
|
7
|
+
normalized.activationId,
|
|
8
|
+
normalized.continuationId,
|
|
9
|
+
normalized.generation
|
|
10
|
+
].join("\u0000");
|
|
11
|
+
}
|
|
12
|
+
export function createProviderContinuation(input) {
|
|
13
|
+
return validateProviderContinuation({
|
|
14
|
+
schemaVersion: 1,
|
|
15
|
+
taskId: text(input.taskId, "Task id"),
|
|
16
|
+
roleName: text(input.roleName, "Role name"),
|
|
17
|
+
runId: text(input.runId, "Run id"),
|
|
18
|
+
identity: validateProviderContinuationIdentity(input.identity),
|
|
19
|
+
...(input.parentContinuationId === undefined
|
|
20
|
+
? {}
|
|
21
|
+
: { parentContinuationId: text(input.parentContinuationId, "Parent Continuation id") }),
|
|
22
|
+
execution: "active",
|
|
23
|
+
outcome: "pending",
|
|
24
|
+
attachment: input.attachment,
|
|
25
|
+
observation: input.observation,
|
|
26
|
+
mayWriteWorkspace: input.mayWriteWorkspace,
|
|
27
|
+
reports: [],
|
|
28
|
+
...(input.providerSequence === undefined ? {} : { lastProviderSequence: input.providerSequence }),
|
|
29
|
+
identityConflict: false,
|
|
30
|
+
createdAt: timestamp(input.observedAt, "Continuation observedAt"),
|
|
31
|
+
updatedAt: input.observedAt
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
export function recordProviderReport(raw, report, providerSequence) {
|
|
35
|
+
const current = validateProviderContinuation(raw);
|
|
36
|
+
if (current.identityConflict)
|
|
37
|
+
return current;
|
|
38
|
+
if (providerSequenceRegresses(current, providerSequence))
|
|
39
|
+
return current;
|
|
40
|
+
const normalized = validateProviderReport(report);
|
|
41
|
+
if (current.reports.some((entry) => entry.reportId === normalized.reportId))
|
|
42
|
+
return current;
|
|
43
|
+
return validateProviderContinuation({
|
|
44
|
+
...current,
|
|
45
|
+
reports: [...current.reports, normalized],
|
|
46
|
+
...(providerSequence === undefined ? {} : { lastProviderSequence: providerSequence }),
|
|
47
|
+
updatedAt: normalized.observedAt
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
export function observeProviderContinuation(raw, input) {
|
|
51
|
+
const current = validateProviderContinuation(raw);
|
|
52
|
+
if (current.identityConflict || providerSequenceRegresses(current, input.providerSequence)) {
|
|
53
|
+
return current;
|
|
54
|
+
}
|
|
55
|
+
const observedAt = timestamp(input.observedAt, "Continuation observedAt");
|
|
56
|
+
if (current.settledAt !== undefined) {
|
|
57
|
+
if (input.execution === "quiescent"
|
|
58
|
+
&& input.outcome === current.outcome
|
|
59
|
+
&& (input.resultRef === undefined || input.resultRef === current.resultRef))
|
|
60
|
+
return current;
|
|
61
|
+
// Exact terminal evidence may fill a result reference that was indexed
|
|
62
|
+
// after settlement, but it can never replace an existing reference.
|
|
63
|
+
if (input.execution === "quiescent"
|
|
64
|
+
&& input.observation === "exact"
|
|
65
|
+
&& input.outcome === current.outcome
|
|
66
|
+
&& current.resultRef === undefined
|
|
67
|
+
&& input.resultRef !== undefined) {
|
|
68
|
+
return validateProviderContinuation({
|
|
69
|
+
...current,
|
|
70
|
+
resultRef: text(input.resultRef, "Result ref"),
|
|
71
|
+
...(input.providerSequence === undefined
|
|
72
|
+
? {}
|
|
73
|
+
: { lastProviderSequence: input.providerSequence }),
|
|
74
|
+
updatedAt: observedAt
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Exact absence/completeness can close writer ownership before the
|
|
78
|
+
// provider's terminal result record arrives. A later exact terminal fact
|
|
79
|
+
// may refine only the unknown outcome; it cannot reopen execution.
|
|
80
|
+
if (current.outcome === "unknown"
|
|
81
|
+
&& input.execution === "quiescent"
|
|
82
|
+
&& input.observation === "exact"
|
|
83
|
+
&& input.outcome !== "pending") {
|
|
84
|
+
return validateProviderContinuation({
|
|
85
|
+
...current,
|
|
86
|
+
outcome: input.outcome,
|
|
87
|
+
...(input.resultRef === undefined ? {} : { resultRef: text(input.resultRef, "Result ref") }),
|
|
88
|
+
...(input.providerSequence === undefined
|
|
89
|
+
? {}
|
|
90
|
+
: { lastProviderSequence: input.providerSequence }),
|
|
91
|
+
updatedAt: observedAt
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return validateProviderContinuation({ ...current, identityConflict: true, updatedAt: observedAt });
|
|
95
|
+
}
|
|
96
|
+
if (input.execution === "quiescent" && input.observation !== "exact") {
|
|
97
|
+
return validateProviderContinuation({
|
|
98
|
+
...current,
|
|
99
|
+
execution: "unknown",
|
|
100
|
+
observation: input.observation,
|
|
101
|
+
attachment: input.attachment,
|
|
102
|
+
// Partial/unavailable evidence cannot release an already-established
|
|
103
|
+
// writer umbrella. Only exact quiescence may prove workspace ownership
|
|
104
|
+
// ended; otherwise cleanup could race a detached provider process.
|
|
105
|
+
mayWriteWorkspace: current.mayWriteWorkspace || input.mayWriteWorkspace,
|
|
106
|
+
...(input.providerSequence === undefined ? {} : { lastProviderSequence: input.providerSequence }),
|
|
107
|
+
updatedAt: observedAt
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return validateProviderContinuation({
|
|
111
|
+
...current,
|
|
112
|
+
execution: input.execution,
|
|
113
|
+
outcome: input.outcome,
|
|
114
|
+
attachment: input.attachment,
|
|
115
|
+
observation: input.observation,
|
|
116
|
+
mayWriteWorkspace: input.mayWriteWorkspace,
|
|
117
|
+
...(input.resultRef === undefined ? {} : { resultRef: text(input.resultRef, "Result ref") }),
|
|
118
|
+
...(input.execution === "quiescent" && input.observation === "exact"
|
|
119
|
+
? { settledAt: observedAt }
|
|
120
|
+
: {}),
|
|
121
|
+
...(input.providerSequence === undefined ? {} : { lastProviderSequence: input.providerSequence }),
|
|
122
|
+
updatedAt: observedAt
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export function detachProviderContinuation(raw, observedAt) {
|
|
126
|
+
const current = validateProviderContinuation(raw);
|
|
127
|
+
if (current.attachment === "detached")
|
|
128
|
+
return current;
|
|
129
|
+
return validateProviderContinuation({
|
|
130
|
+
...current,
|
|
131
|
+
attachment: "detached",
|
|
132
|
+
updatedAt: timestamp(observedAt, "Continuation detachedAt")
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
export function continuationOwnsWriterUmbrella(value) {
|
|
136
|
+
const continuation = validateProviderContinuation(value);
|
|
137
|
+
return continuation.mayWriteWorkspace
|
|
138
|
+
&& continuation.identityConflict === false
|
|
139
|
+
&& (continuation.execution === "active" || continuation.execution === "unknown");
|
|
140
|
+
}
|
|
141
|
+
export function validateProviderContinuation(value) {
|
|
142
|
+
if (value.schemaVersion !== 1)
|
|
143
|
+
throw new Error("Provider Continuation schemaVersion must be 1.");
|
|
144
|
+
text(value.taskId, "Task id");
|
|
145
|
+
text(value.roleName, "Role name");
|
|
146
|
+
text(value.runId, "Run id");
|
|
147
|
+
validateProviderContinuationIdentity(value.identity);
|
|
148
|
+
if (!["active", "quiescent", "unknown"].includes(value.execution)) {
|
|
149
|
+
throw new Error("Provider Continuation execution is invalid.");
|
|
150
|
+
}
|
|
151
|
+
if (!["pending", "succeeded", "failed", "cancelled", "unknown"].includes(value.outcome)) {
|
|
152
|
+
throw new Error("Provider Continuation outcome is invalid.");
|
|
153
|
+
}
|
|
154
|
+
if (value.attachment !== "attached" && value.attachment !== "detached") {
|
|
155
|
+
throw new Error("Provider Continuation attachment is invalid.");
|
|
156
|
+
}
|
|
157
|
+
if (!["exact", "partial", "unavailable"].includes(value.observation)) {
|
|
158
|
+
throw new Error("Provider Continuation observation quality is invalid.");
|
|
159
|
+
}
|
|
160
|
+
if (typeof value.mayWriteWorkspace !== "boolean" || typeof value.identityConflict !== "boolean") {
|
|
161
|
+
throw new Error("Provider Continuation flags are invalid.");
|
|
162
|
+
}
|
|
163
|
+
const reports = value.reports.map(validateProviderReport);
|
|
164
|
+
if (new Set(reports.map((entry) => entry.reportId)).size !== reports.length) {
|
|
165
|
+
throw new Error("Provider Continuation reports contain duplicate identity.");
|
|
166
|
+
}
|
|
167
|
+
timestamp(value.createdAt, "Provider Continuation createdAt");
|
|
168
|
+
timestamp(value.updatedAt, "Provider Continuation updatedAt");
|
|
169
|
+
if (value.settledAt !== undefined) {
|
|
170
|
+
timestamp(value.settledAt, "Provider Continuation settledAt");
|
|
171
|
+
if (value.execution !== "quiescent" || value.observation !== "exact") {
|
|
172
|
+
throw new Error("Settled Provider Continuation requires exact quiescence.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (value.execution === "active" && value.outcome !== "pending") {
|
|
176
|
+
throw new Error("Active Provider Continuation outcome must remain pending.");
|
|
177
|
+
}
|
|
178
|
+
if (value.lastProviderSequence !== undefined)
|
|
179
|
+
sequence(value.lastProviderSequence);
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
export function validateProviderContinuationIdentity(value) {
|
|
183
|
+
text(value.providerNamespace, "Provider namespace");
|
|
184
|
+
text(value.accountScope, "Provider account scope");
|
|
185
|
+
text(value.conversationId, "Provider Conversation id");
|
|
186
|
+
text(value.activationId, "Provider Activation id");
|
|
187
|
+
text(value.continuationId, "Provider Continuation id");
|
|
188
|
+
if (!Number.isSafeInteger(value.generation) || value.generation < 1) {
|
|
189
|
+
throw new Error("Provider Continuation generation is invalid.");
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
193
|
+
function validateProviderReport(value) {
|
|
194
|
+
return {
|
|
195
|
+
reportId: text(value.reportId, "Provider report id"),
|
|
196
|
+
...(value.resultRef === undefined ? {} : { resultRef: text(value.resultRef, "Result ref") }),
|
|
197
|
+
...(value.providerDeliveryRef === undefined
|
|
198
|
+
? {}
|
|
199
|
+
: { providerDeliveryRef: text(value.providerDeliveryRef, "Provider delivery ref") }),
|
|
200
|
+
observedAt: timestamp(value.observedAt, "Provider report observedAt")
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function providerSequenceRegresses(current, incoming) {
|
|
204
|
+
if (incoming === undefined)
|
|
205
|
+
return false;
|
|
206
|
+
sequence(incoming);
|
|
207
|
+
return current.lastProviderSequence !== undefined && incoming < current.lastProviderSequence;
|
|
208
|
+
}
|
|
209
|
+
function sequence(value) {
|
|
210
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
211
|
+
throw new Error("Provider sequence is invalid.");
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
function text(value, label) {
|
|
215
|
+
if (typeof value !== "string" || value.includes("\0") || value.trim().length === 0) {
|
|
216
|
+
throw new Error(`${label} is invalid.`);
|
|
217
|
+
}
|
|
218
|
+
return value.trim();
|
|
219
|
+
}
|
|
220
|
+
function timestamp(value, label) {
|
|
221
|
+
const normalized = text(value, label);
|
|
222
|
+
if (!Number.isFinite(Date.parse(normalized)))
|
|
223
|
+
throw new Error(`${label} must be a timestamp.`);
|
|
224
|
+
return normalized;
|
|
225
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { providerContinuationKey } from "./providerContinuation.js";
|
|
3
|
+
import { projectProviderContinuations } from "./runtimeContinuationProjection.js";
|
|
4
|
+
import { reconcileKnownDetachedContinuations } from "./providerRuntimeReconciler.js";
|
|
5
|
+
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "./runtimeObservation.js";
|
|
6
|
+
/**
|
|
7
|
+
* Low-frequency, metadata-only recovery for already-known detached children.
|
|
8
|
+
* No launch/model method is reachable through this object. The backoff is an
|
|
9
|
+
* advisory cache; committed observations remain the only durable authority.
|
|
10
|
+
*/
|
|
11
|
+
export class ProviderContinuationReconciliationService {
|
|
12
|
+
store;
|
|
13
|
+
sink;
|
|
14
|
+
metadata;
|
|
15
|
+
#schedules = new Map();
|
|
16
|
+
constructor(store, sink, metadata) {
|
|
17
|
+
this.store = store;
|
|
18
|
+
this.sink = sink;
|
|
19
|
+
this.metadata = metadata;
|
|
20
|
+
}
|
|
21
|
+
async reconcile(now) {
|
|
22
|
+
const changedTaskIds = new Set();
|
|
23
|
+
for (const task of this.store.listTasks().filter((entry) => entry.status === "active")) {
|
|
24
|
+
const events = this.store.listEvents(task.id);
|
|
25
|
+
const groups = groupDetachedContinuations(projectProviderContinuations(events), events.map(runtimeObservationFromTaskEvent)
|
|
26
|
+
.filter((entry) => entry !== null));
|
|
27
|
+
for (const [groupKey, candidates] of groups) {
|
|
28
|
+
const group = candidates.map(({ continuation }) => continuation);
|
|
29
|
+
const previous = this.#schedules.get(groupKey);
|
|
30
|
+
if (previous !== undefined && Date.parse(previous.nextReconcileAt) > now.getTime()) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
let result;
|
|
34
|
+
try {
|
|
35
|
+
result = await reconcileKnownDetachedContinuations({
|
|
36
|
+
port: this.metadata,
|
|
37
|
+
continuations: group,
|
|
38
|
+
...(previous === undefined ? {} : { previous }),
|
|
39
|
+
now
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// One malformed identity group must not restart or block the
|
|
44
|
+
// Controller. Keep it writer-owned and retry through the same
|
|
45
|
+
// bounded, metadata-only schedule.
|
|
46
|
+
this.#schedules.set(groupKey, failureSchedule(groupKey, previous, now));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (result.schedule === null)
|
|
50
|
+
this.#schedules.delete(groupKey);
|
|
51
|
+
else
|
|
52
|
+
this.#schedules.set(groupKey, result.schedule);
|
|
53
|
+
for (let index = 0; index < group.length; index += 1) {
|
|
54
|
+
const before = group[index];
|
|
55
|
+
const after = result.continuations[index];
|
|
56
|
+
if (sameContinuationState(before, after))
|
|
57
|
+
continue;
|
|
58
|
+
const disposition = this.sink.observeRuntimeObservation(reconciliationObservation(after, candidates[index].fence, now), now);
|
|
59
|
+
if (disposition === "applied")
|
|
60
|
+
changedTaskIds.add(after.taskId);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return Object.freeze([...changedTaskIds].sort());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function failureSchedule(key, previous, now) {
|
|
68
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
69
|
+
const errors = (previous?.consecutiveErrors ?? 0) + 1;
|
|
70
|
+
const delay = Math.min(5 * 60_000, 2_000 * (2 ** Math.min(attempts - 1, 8)));
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
key,
|
|
73
|
+
attempts,
|
|
74
|
+
consecutiveErrors: errors,
|
|
75
|
+
nextReconcileAt: new Date(now.getTime() + delay).toISOString(),
|
|
76
|
+
...(errors < 5
|
|
77
|
+
? {}
|
|
78
|
+
: { circuitOpenUntil: new Date(now.getTime() + 5 * 60_000).toISOString() })
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function groupDetachedContinuations(continuations, observations) {
|
|
82
|
+
const groups = new Map();
|
|
83
|
+
for (const continuation of continuations) {
|
|
84
|
+
if (continuation.attachment !== "detached"
|
|
85
|
+
|| continuation.identityConflict
|
|
86
|
+
|| (continuation.execution !== "active" && continuation.execution !== "unknown")) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const key = [
|
|
90
|
+
continuation.identity.providerNamespace,
|
|
91
|
+
continuation.identity.accountScope,
|
|
92
|
+
continuation.identity.conversationId,
|
|
93
|
+
continuation.identity.activationId
|
|
94
|
+
].join("\u0000");
|
|
95
|
+
const source = [...observations].reverse().find((observation) => (observation.kind.startsWith("continuation.")
|
|
96
|
+
&& observation.fence.driverId === continuation.identity.providerNamespace
|
|
97
|
+
&& observation.fence.agentId === continuation.identity.accountScope
|
|
98
|
+
&& observation.fence.conversationId === continuation.identity.conversationId
|
|
99
|
+
&& observation.fence.activationId === continuation.identity.activationId
|
|
100
|
+
&& observation.fence.continuationId === continuation.identity.continuationId
|
|
101
|
+
&& observation.fence.continuationGeneration === continuation.identity.generation));
|
|
102
|
+
// A projected continuation without its original durable fence cannot be
|
|
103
|
+
// safely attached to a live Run. Keep ownership conservative and let the
|
|
104
|
+
// malformed identity remain visible instead of synthesizing a receipt.
|
|
105
|
+
if (source === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
const group = groups.get(key) ?? [];
|
|
108
|
+
group.push({ continuation, fence: source.fence });
|
|
109
|
+
groups.set(key, group);
|
|
110
|
+
}
|
|
111
|
+
return groups;
|
|
112
|
+
}
|
|
113
|
+
function sameContinuationState(left, right) {
|
|
114
|
+
return left.execution === right.execution
|
|
115
|
+
&& left.outcome === right.outcome
|
|
116
|
+
&& left.attachment === right.attachment
|
|
117
|
+
&& left.observation === right.observation
|
|
118
|
+
&& left.mayWriteWorkspace === right.mayWriteWorkspace
|
|
119
|
+
&& left.resultRef === right.resultRef
|
|
120
|
+
&& left.lastProviderSequence === right.lastProviderSequence
|
|
121
|
+
&& left.identityConflict === right.identityConflict;
|
|
122
|
+
}
|
|
123
|
+
function reconciliationObservation(continuation, sourceFence, now) {
|
|
124
|
+
const key = providerContinuationKey(continuation.identity);
|
|
125
|
+
const state = [
|
|
126
|
+
continuation.execution,
|
|
127
|
+
continuation.outcome,
|
|
128
|
+
continuation.observation,
|
|
129
|
+
continuation.mayWriteWorkspace ? "writer" : "read-only",
|
|
130
|
+
continuation.resultRef ?? "none",
|
|
131
|
+
continuation.lastProviderSequence ?? "none"
|
|
132
|
+
].join(":");
|
|
133
|
+
const digest = createHash("sha256").update(`${key}\u0000${state}`).digest("hex");
|
|
134
|
+
const settled = continuation.execution === "quiescent"
|
|
135
|
+
&& continuation.observation === "exact";
|
|
136
|
+
return createRuntimeObservation({
|
|
137
|
+
schemaVersion: 2,
|
|
138
|
+
eventId: `continuation-reconcile:${digest}`,
|
|
139
|
+
semanticKey: `continuation-reconcile:${digest}`,
|
|
140
|
+
kind: settled ? "continuation.settled" : "continuation.started",
|
|
141
|
+
authority: "provider-structured",
|
|
142
|
+
receivedAt: now.toISOString(),
|
|
143
|
+
observedAt: now.toISOString(),
|
|
144
|
+
...(continuation.lastProviderSequence === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { sequence: continuation.lastProviderSequence }),
|
|
147
|
+
fence: {
|
|
148
|
+
...sourceFence,
|
|
149
|
+
taskId: continuation.taskId,
|
|
150
|
+
roleName: continuation.roleName,
|
|
151
|
+
runId: continuation.runId,
|
|
152
|
+
agentId: continuation.identity.accountScope,
|
|
153
|
+
driverId: continuation.identity.providerNamespace,
|
|
154
|
+
conversationId: continuation.identity.conversationId,
|
|
155
|
+
activationId: continuation.identity.activationId,
|
|
156
|
+
nativeSessionId: continuation.identity.conversationId,
|
|
157
|
+
continuationId: continuation.identity.continuationId,
|
|
158
|
+
continuationGeneration: continuation.identity.generation,
|
|
159
|
+
...(continuation.parentContinuationId === undefined
|
|
160
|
+
? {}
|
|
161
|
+
: { parentContinuationId: continuation.parentContinuationId })
|
|
162
|
+
},
|
|
163
|
+
payload: {
|
|
164
|
+
execution: continuation.execution,
|
|
165
|
+
outcome: continuation.outcome,
|
|
166
|
+
attachment: "detached",
|
|
167
|
+
observationQuality: continuation.observation,
|
|
168
|
+
mayWriteWorkspace: continuation.mayWriteWorkspace,
|
|
169
|
+
...(continuation.resultRef === undefined ? {} : { resultRef: continuation.resultRef })
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|