@zq-silk/yui 0.15.11 → 0.15.12
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 +8 -4
- package/ARCHITECTURE.zh-CN.md +5 -2
- package/README.md +13 -5
- package/dist/agentRun/agentRun.js +3 -0
- package/dist/cli/commandCatalog.js +38 -10
- package/dist/cli/interactionPolicy.js +4 -0
- package/dist/cli/managedDiagnostics.js +1 -1
- package/dist/cli.js +46 -15
- package/dist/commands/executionAuditCommands.js +10 -0
- package/dist/commands/globalRoleCommands.js +27 -2
- package/dist/commands/projectCommands.js +44 -15
- package/dist/commands/taskCommands.js +95 -33
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskOverviewCommand.js +4 -3
- package/dist/commands/taskPublicationAdoptCommand.js +127 -0
- package/dist/commands/taskPublicationCommands.js +11 -2
- package/dist/commands/taskPublicationVerifyCommand.js +23 -39
- package/dist/commands/taskRemoteDeliveryCommand.js +18 -7
- package/dist/context/runContextPack.js +3 -0
- package/dist/context/taskCatalog.js +187 -0
- package/dist/context/taskContext.js +17 -4
- package/dist/controller/controller.js +11 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +80 -19
- package/dist/controller/globalInputDelivery.js +13 -0
- package/dist/controller/providerRetryAdmission.js +100 -0
- package/dist/controller/providerRetryDelivery.js +218 -0
- package/dist/controller/runtime.js +36 -1
- package/dist/integration/gitIntegrationService.js +19 -6
- package/dist/lifecycle/exactRunTerminalization.js +4 -1
- package/dist/message/globalProviderRetry.js +15 -0
- package/dist/observability/executionAudit.js +19 -0
- package/dist/repository/gitWorkspace.js +371 -105
- package/dist/repository/projectMaintenanceLock.js +75 -18
- package/dist/repository/taskWorkspaceCoordinator.js +71 -124
- package/dist/repository/taskWorkspacePreparer.js +86 -24
- package/dist/repository/workspaceCleanupInspection.js +187 -0
- package/dist/runtime/agentError.js +5 -3
- package/dist/runtime/agentHost.js +28 -11
- package/dist/runtime/builtinAgentErrorMappers.js +91 -0
- package/dist/runtime/codexAppServerRuntime.js +34 -3
- package/dist/runtime/providerControl.js +5 -1
- package/dist/runtime/providerRetry.js +198 -0
- package/dist/runtime/providerRuntimeIdentity.js +28 -2
- package/dist/runtime/sessionTokenMetrics.js +15 -5
- package/dist/runtime/structuredProviderHost.js +6 -2
- package/dist/runtime/taskUsageMetrics.js +275 -0
- package/dist/scheduler/activeRoleRunDelivery.js +12 -0
- package/dist/scheduler/leaderWakeupProcessor.js +5 -0
- package/dist/scheduler/taskExecutionProjection.js +26 -5
- package/dist/scheduler/taskObservabilityProjection.js +6 -44
- package/dist/storage/sqliteSchema.js +31 -0
- package/dist/storage/sqliteStore.js +17 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskCatalog.js +123 -0
- package/dist/storage/taskStore.js +2 -0
- package/dist/task/archiveDiagnostics.js +1 -0
- package/dist/task/archivePreflight.js +124 -0
- package/dist/task/publicationAdoption.js +56 -0
- package/dist/task/publicationReference.js +10 -0
- package/dist/task/remoteDelivery.js +31 -16
- package/dist/web/assets/client/app.js +92 -17
- package/dist/web/assets/client/components.js +55 -13
- package/dist/web/assets/client/i18n.js +72 -4
- package/dist/web/assets/client/taskSurface.js +2 -1
- package/dist/web/assets/client/view.js +35 -7
- package/dist/web/assets/shell.js +6 -0
- package/dist/web/assets/styles/layout.js +7 -0
- package/dist/web/webServer.js +14 -3
- package/dist/web/webSnapshot.js +12 -3
- package/dist/workspace/cleanupInspection.js +63 -0
- package/dist/workspace/workItemChangeSetManager.js +110 -50
- package/docs/agent-result-consumption.md +4 -0
- package/docs/agent-result-consumption.zh-CN.md +3 -0
- package/docs/agent-runtime-drivers.md +7 -0
- package/docs/agent-runtime-drivers.zh-CN.md +5 -0
- package/docs/architecture/README.md +2 -0
- package/docs/architecture/README.zh-CN.md +3 -1
- package/docs/architecture/capabilities-and-resources.md +30 -5
- package/docs/architecture/capabilities-and-resources.zh-CN.md +23 -3
- package/docs/managed-turn-and-session-runtime.md +47 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +40 -0
- package/docs/observability/README.md +62 -0
- package/docs/observability/README.zh-CN.md +47 -0
- package/docs/project-refresh.md +77 -0
- package/docs/project-refresh.zh-CN.md +59 -0
- package/docs/provider-retry.md +70 -0
- package/docs/task-delivery.md +133 -13
- package/docs/task-delivery.zh-CN.md +99 -10
- package/docs/task-discovery.md +102 -0
- package/docs/task-discovery.zh-CN.md +86 -0
- package/docs/testing/verification-levels.md +16 -0
- package/docs/testing/verification-levels.zh-CN.md +13 -1
- package/i18n/README.zh-CN.md +13 -7
- package/package.json +1 -1
- package/skills/yui-leader/references/execution.md +3 -2
- package/skills/yui-operator/SKILL.md +13 -2
- package/skills/yui-reviewer/SKILL.md +4 -0
- package/skills/yui-runtime/SKILL.md +17 -2
- package/skills/yui-runtime/references/publication.md +26 -4
- package/skills/yui-runtime/references/recovery.md +24 -0
|
@@ -2,6 +2,8 @@ import { sendAgentHostRunControl, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime
|
|
|
2
2
|
import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
3
3
|
import { hasRuntimeCleanupObligation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
4
4
|
import { markGlobalRoleMessageNotDelivered } from "../message/message.js";
|
|
5
|
+
import { providerRetryPending } from "../runtime/providerRetry.js";
|
|
6
|
+
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
5
7
|
/** Messages are the only queue. Host's atomic begin arbitrates native writers. */
|
|
6
8
|
export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
7
9
|
for (const role of store.listGlobalRoles()) {
|
|
@@ -11,6 +13,15 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
11
13
|
if (pending.length === 0 || hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget({ scope: "global", roleName: role.name }))))
|
|
12
14
|
continue;
|
|
13
15
|
let sessions = store.getGlobalRoleSessionSet(role.name);
|
|
16
|
+
if (sessions?.providerBinding?.retry !== undefined && !providerRetryPending(sessions.providerBinding)) {
|
|
17
|
+
store.transaction(tx => settleGlobalRetryInput(tx, role.name, tx.getGlobalRoleSessionSet(role.name)?.providerBinding, new Date()));
|
|
18
|
+
pending = store.listGlobalRoleMessages(role.name).filter(message => message.delivery === undefined && message.notDelivered === undefined
|
|
19
|
+
&& (message.inputControl?.action === "queue" || message.interruptThen !== undefined));
|
|
20
|
+
if (pending.length === 0)
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (providerRetryPending(sessions?.providerBinding))
|
|
24
|
+
continue;
|
|
14
25
|
const current = sessions?.providerBinding?.run;
|
|
15
26
|
const obsolete = pending.filter(message => {
|
|
16
27
|
const claim = message.interruptThen;
|
|
@@ -99,6 +110,8 @@ export async function deliverGlobalInputs(home, store, ensure, onError) {
|
|
|
99
110
|
if (latest === undefined || latest.delivery !== undefined || latest.notDelivered !== undefined)
|
|
100
111
|
return;
|
|
101
112
|
const turn = tx.getGlobalRoleSessionSet(role.name)?.providerBinding?.run;
|
|
113
|
+
if (providerRetryPending(tx.getGlobalRoleSessionSet(role.name)?.providerBinding))
|
|
114
|
+
return;
|
|
102
115
|
// A later canonical acceptance wins over transport diagnostics.
|
|
103
116
|
if (turn?.attemptId === attemptId && ["accepted", "completed", "failed", "cancelled"].includes(turn.status))
|
|
104
117
|
return;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { admitProviderRetry, cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
|
|
2
|
+
import { blockingProviderContinuations } from "../runtime/runtimeContinuationProjection.js";
|
|
3
|
+
import { validateExactRunReviewRound } from "../lifecycle/exactRunTerminalization.js";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
export function ownedProviderInput(input) {
|
|
6
|
+
if (input.runId !== undefined && input.taskId !== undefined) {
|
|
7
|
+
return { kind: "run", taskId: input.taskId, runId: input.runId };
|
|
8
|
+
}
|
|
9
|
+
const wake = /^notification:([^/]+)\/([^/]+)\//u.exec(input.attemptId);
|
|
10
|
+
if (wake !== null && wake[1] === input.taskId) {
|
|
11
|
+
return { kind: "wake", taskId: wake[1], wakeId: wake[2] };
|
|
12
|
+
}
|
|
13
|
+
const message = /^global-input:([^/]+)\/([^/]+)$/u.exec(input.attemptId);
|
|
14
|
+
if (message !== null && message[1] === input.roleName && input.taskId === undefined) {
|
|
15
|
+
return { kind: "message", roleName: input.roleName, messageId: message[2] };
|
|
16
|
+
}
|
|
17
|
+
return input.boundedText === undefined ? undefined : { kind: "text", text: input.boundedText };
|
|
18
|
+
}
|
|
19
|
+
export function retryIntentBlocker(store, input, retry) {
|
|
20
|
+
if (input.taskId === undefined) {
|
|
21
|
+
const role = store.getGlobalRole(input.roleName);
|
|
22
|
+
const sessions = store.getGlobalRoleSessionSet(input.roleName);
|
|
23
|
+
if (role === null || sessions?.activeAgentId !== role.activeAgentId)
|
|
24
|
+
return "role-changed";
|
|
25
|
+
if (sessions.sessions[sessions.activeAgentId]?.effective.sourceDesiredRevision !== role.launchRevision)
|
|
26
|
+
return "role-configuration-changed";
|
|
27
|
+
if (store.listGlobalRoleMessages(input.roleName).some(message => Date.parse(message.createdAt) > Date.parse(retry.intentAt)
|
|
28
|
+
&& message.delivery === undefined && message.notDelivered === undefined))
|
|
29
|
+
return "new-user-input";
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const task = store.getTask(input.taskId);
|
|
33
|
+
if (task === null || !["active", "draft"].includes(task.status)
|
|
34
|
+
|| task.executionGate.state !== "enabled")
|
|
35
|
+
return "task-execution-stopped";
|
|
36
|
+
const role = store.getRole(input.taskId, input.roleName);
|
|
37
|
+
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
38
|
+
if (role === null || sessions?.activeAgentId !== role.activeAgentId)
|
|
39
|
+
return "role-changed";
|
|
40
|
+
if (sessions.sessions[sessions.activeAgentId]?.effective.sourceDesiredRevision !== role.launchRevision)
|
|
41
|
+
return "role-configuration-changed";
|
|
42
|
+
if (store.listMessages(input.taskId).some(message => (message.kind === "operator" || message.kind === "user")
|
|
43
|
+
&& Date.parse(message.createdAt) > Date.parse(retry.intentAt)))
|
|
44
|
+
return "new-user-input";
|
|
45
|
+
if (blockingProviderContinuations(store.listEvents(input.taskId)).some(c => c.roleName === input.roleName && c.identity.conversationId === retry.nativeSessionId)) {
|
|
46
|
+
return "background-work-unsettled";
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
/** Shared by automatic recovery and every ordinary Host submit. The native
|
|
51
|
+
* writer CAS remains the only grant; a scheduled successor is not a grant. */
|
|
52
|
+
export function admitOwnedProviderInput(store, binding, input) {
|
|
53
|
+
const retry = binding.retry;
|
|
54
|
+
const sameReviewSuccessor = retry?.successorReviewRoundId !== undefined && input.taskId !== undefined && input.runId !== undefined
|
|
55
|
+
&& store.getRun(input.taskId, input.runId)?.reviewRoundId === retry.successorReviewRoundId;
|
|
56
|
+
if (retry !== undefined && !providerRetryPending(binding)
|
|
57
|
+
&& (input.attemptId.startsWith("provider-retry:") || input.runId !== undefined && input.runId === retry.successorRunId
|
|
58
|
+
|| sameReviewSuccessor)) {
|
|
59
|
+
throw new Error(`Provider retry is ${retry.status}; this scheduled successor cannot regain admission.`);
|
|
60
|
+
}
|
|
61
|
+
if (!providerRetryPending(binding) || retry === undefined)
|
|
62
|
+
return binding;
|
|
63
|
+
const automatic = input.attemptId === providerRetryAttemptId(retry)
|
|
64
|
+
|| input.runId !== undefined && input.runId === retry.successorRunId
|
|
65
|
+
|| input.runId !== undefined && input.taskId !== undefined && retry.successorReviewRoundId !== undefined
|
|
66
|
+
&& store.getRun(input.taskId, input.runId)?.reviewRoundId === retry.successorReviewRoundId
|
|
67
|
+
|| retry.status === "in-flight" && input.attemptId === retry.currentAttemptId;
|
|
68
|
+
if (!automatic)
|
|
69
|
+
return cancelProviderRetry(binding, "superseded-by-explicit-input", input.now.getTime());
|
|
70
|
+
const blocker = retryIntentBlocker(store, input, retry);
|
|
71
|
+
if (blocker !== undefined)
|
|
72
|
+
throw new Error(`Provider retry admission stopped: ${blocker}.`);
|
|
73
|
+
if (input.retrySupported !== true)
|
|
74
|
+
throw new Error("This Host cannot prove safe same-Session recovery; use explicit recovery.");
|
|
75
|
+
if (binding.goal?.status === "active")
|
|
76
|
+
throw new Error("Provider still owns an autonomous Goal; inspect its exact continuation.");
|
|
77
|
+
if (input.taskId !== undefined && input.runId !== undefined) {
|
|
78
|
+
const run = store.getRun(input.taskId, input.runId);
|
|
79
|
+
const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
|
|
80
|
+
if (run === null || run.status !== "active"
|
|
81
|
+
|| !isDeepStrictEqual(run.effective, sessions?.sessions[input.agentId]?.effective)) {
|
|
82
|
+
throw new Error("Retry execution no longer matches its fixed native Session.");
|
|
83
|
+
}
|
|
84
|
+
if (run.workspace !== undefined && !isDeepStrictEqual(store.getManagedWorkspace(run.workspace.owner), run.workspace)) {
|
|
85
|
+
throw new Error("Retry workspace authority changed.");
|
|
86
|
+
}
|
|
87
|
+
if (run.workItemId !== undefined) {
|
|
88
|
+
const item = store.getWorkItem(input.taskId, run.workItemId);
|
|
89
|
+
if (item?.status !== "open" || run.purpose !== "review" && run.executionLaneId === undefined
|
|
90
|
+
&& item.assignee !== run.roleName)
|
|
91
|
+
throw new Error("Retry WorkItem responsibility changed.");
|
|
92
|
+
}
|
|
93
|
+
if (validateExactRunReviewRound(store, run).disposition !== "applied") {
|
|
94
|
+
throw new Error("Retry Review no longer matches its exact Round and frozen candidate.");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const admitted = admitProviderRetry(binding, { attemptId: input.attemptId, at: input.now.getTime() });
|
|
98
|
+
return input.runId === undefined ? admitted
|
|
99
|
+
: { ...admitted, retry: { ...admitted.retry, successorRunId: input.runId } };
|
|
100
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { runTaskCommand } from "../commands/taskCommands.js";
|
|
3
|
+
import { retryIntentBlocker } from "./providerRetryAdmission.js";
|
|
4
|
+
import { cancelProviderRetry, providerRetryAttemptId, providerRetryPending } from "../runtime/providerRetry.js";
|
|
5
|
+
import { currentProviderConversation } from "../runtime/providerRuntimeIdentity.js";
|
|
6
|
+
import { runtimeLifecycleTarget, hasRuntimeCleanupObligation } from "../runtime/lifecycleReservation.js";
|
|
7
|
+
import { sendAgentHostRunControl, inspectAgentHostSocket, agentHostControlSocketPath, AGENT_HOST_CONTROL_PROTOCOL } from "../runtime/agentHost.js";
|
|
8
|
+
import { redactAgentErrorText } from "../runtime/agentError.js";
|
|
9
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
10
|
+
import { enqueueWork } from "../coordination/workMailboxQueue.js";
|
|
11
|
+
import { settleGlobalRetryInput } from "../message/globalProviderRetry.js";
|
|
12
|
+
/** Hooks in the existing Controller pass and nearest-deadline timer. SQLite's
|
|
13
|
+
* current Provider input is the only pending intent; nothing is queued here. */
|
|
14
|
+
export function createProviderRetryHooks(home, store, ports = {}) {
|
|
15
|
+
const anchors = new Map();
|
|
16
|
+
const inspectedUnknown = new Set();
|
|
17
|
+
const inspect = ports.inspect ?? ((target) => inspectAgentHostSocket(agentHostControlSocketPath(target), 250));
|
|
18
|
+
const now = (retry) => {
|
|
19
|
+
const wall = (ports.now ?? (() => new Date()))().getTime();
|
|
20
|
+
const monotonic = (ports.monotonicNow ?? (() => performance.now()))();
|
|
21
|
+
let anchor = anchors.get(retry.chainId);
|
|
22
|
+
if (anchor === undefined) {
|
|
23
|
+
anchor = { wall: Math.max(wall, Date.parse(retry.updatedAt)), monotonic };
|
|
24
|
+
anchors.set(retry.chainId, anchor);
|
|
25
|
+
}
|
|
26
|
+
return Math.max(wall, anchor.wall + Math.max(0, monotonic - anchor.monotonic));
|
|
27
|
+
};
|
|
28
|
+
const load = (owner) => owner.scope === "task"
|
|
29
|
+
? store.getTaskRoleSessionSet(owner.taskId, owner.roleName) : store.getGlobalRoleSessionSet(owner.roleName);
|
|
30
|
+
const save = (set) => {
|
|
31
|
+
if (set.owner.scope === "task")
|
|
32
|
+
store.saveTaskRoleSessionSet(set);
|
|
33
|
+
else
|
|
34
|
+
store.saveGlobalRoleSessionSet(set);
|
|
35
|
+
};
|
|
36
|
+
const stop = (set, reason, at, exhausted = false) => {
|
|
37
|
+
store.transaction(() => {
|
|
38
|
+
const current = load(set.owner);
|
|
39
|
+
if (current === null || current.providerBinding?.retry?.chainId !== set.providerBinding?.retry?.chainId)
|
|
40
|
+
return;
|
|
41
|
+
const binding = current.providerBinding;
|
|
42
|
+
if (!providerRetryPending(binding))
|
|
43
|
+
return;
|
|
44
|
+
const cancelled = cancelProviderRetry(binding, reason, at);
|
|
45
|
+
save({ ...current, providerBinding: exhausted
|
|
46
|
+
? { ...cancelled, retry: { ...cancelled.retry, status: "exhausted" } } : cancelled });
|
|
47
|
+
const owner = set.owner;
|
|
48
|
+
if (owner.scope === "global")
|
|
49
|
+
settleGlobalRetryInput(store, owner.roleName, load(owner)?.providerBinding, new Date(at));
|
|
50
|
+
if (owner.scope === "task") {
|
|
51
|
+
const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-stopped", {
|
|
52
|
+
roleName: owner.roleName, chainId: binding.retry.chainId, reason
|
|
53
|
+
}, new Date(at));
|
|
54
|
+
store.saveEvent(owner.taskId, event);
|
|
55
|
+
enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
|
|
56
|
+
: { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const key = (set) => set.owner.scope === "task"
|
|
61
|
+
? `role:${encodeURIComponent(set.owner.taskId)}/${encodeURIComponent(set.owner.roleName)}`
|
|
62
|
+
: `global-role:${encodeURIComponent(set.owner.roleName)}`;
|
|
63
|
+
return {
|
|
64
|
+
deadlines() {
|
|
65
|
+
return store.listProviderRetrySessions().flatMap(set => {
|
|
66
|
+
const retry = set.providerBinding.retry;
|
|
67
|
+
if (retry.status !== "waiting")
|
|
68
|
+
return [];
|
|
69
|
+
// Busy or already-queued work awaits the next ordinary pass without a
|
|
70
|
+
// zero-delay timer loop. Waiting never consumes an execution attempt.
|
|
71
|
+
const wall = (ports.now ?? (() => new Date()))().getTime();
|
|
72
|
+
const effective = now(retry);
|
|
73
|
+
return [{ key: key(set), at: wall + Math.max(250, (retry.successorAutomatic === false ? Date.parse(retry.nextEligibleAt)
|
|
74
|
+
: Math.min(Date.parse(retry.nextEligibleAt), Date.parse(retry.deadline))) - effective) }];
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
async reconcile() {
|
|
78
|
+
const current = store.listProviderRetrySessions();
|
|
79
|
+
const activeChains = new Set(current.map(set => set.providerBinding.retry.chainId));
|
|
80
|
+
for (const id of anchors.keys())
|
|
81
|
+
if (!activeChains.has(id))
|
|
82
|
+
anchors.delete(id);
|
|
83
|
+
const pendingAttempts = new Set(current.map(set => set.providerBinding.retry.currentAttemptId));
|
|
84
|
+
for (const id of inspectedUnknown)
|
|
85
|
+
if (!pendingAttempts.has(id))
|
|
86
|
+
inspectedUnknown.delete(id);
|
|
87
|
+
for (const set of current) {
|
|
88
|
+
const owner = set.owner;
|
|
89
|
+
const binding = set.providerBinding;
|
|
90
|
+
const retry = binding.retry;
|
|
91
|
+
const at = now(retry);
|
|
92
|
+
const scoped = { ...(owner.scope === "task" ? { taskId: owner.taskId } : {}), roleName: owner.roleName };
|
|
93
|
+
const session = set.sessions[set.activeAgentId];
|
|
94
|
+
const blocker = retryIntentBlocker(store, scoped, retry);
|
|
95
|
+
if (blocker !== undefined && blocker !== "background-work-unsettled") {
|
|
96
|
+
stop(set, blocker, at);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (binding.retryDisabled && retry.successorAutomatic !== false || binding.authority.owner !== "controller"
|
|
100
|
+
|| binding.authority.epoch !== retry.authorityEpoch
|
|
101
|
+
|| currentProviderConversation(binding).conversationId !== retry.nativeSessionId
|
|
102
|
+
|| session?.status !== "active" || session.nativeSessionId !== retry.nativeSessionId
|
|
103
|
+
|| hasRuntimeCleanupObligation(store.getWorkMailbox(runtimeLifecycleTarget(owner)))) {
|
|
104
|
+
stop(set, "session-or-authority-changed", at);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// Never replay an admitted attempt. Existing exact runtime queries and
|
|
108
|
+
// terminal ingress resolve it, even across Controller restart.
|
|
109
|
+
if (retry.status === "in-flight") {
|
|
110
|
+
if (binding.run?.status === "delivery-unknown" && retry.currentAttemptId !== undefined
|
|
111
|
+
&& !inspectedUnknown.has(retry.currentAttemptId)) {
|
|
112
|
+
inspectedUnknown.add(retry.currentAttemptId);
|
|
113
|
+
let detail;
|
|
114
|
+
try {
|
|
115
|
+
const host = await inspect({ home, ...scoped, scope: owner.scope });
|
|
116
|
+
detail = host.nativeSessionId === retry.nativeSessionId && host.attemptId === retry.currentAttemptId
|
|
117
|
+
? `Exact Host query: ${host.state}; awaiting original canonical Provider evidence.`
|
|
118
|
+
: "Host query did not prove the original Session/input identity; no replay.";
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
detail = `Original Host query unavailable; no replay: ${redactAgentErrorText(error instanceof Error ? error.message : String(error))}`;
|
|
122
|
+
}
|
|
123
|
+
store.transaction(() => {
|
|
124
|
+
const latest = load(owner);
|
|
125
|
+
if (latest === null || latest.providerBinding == null || latest.providerBinding.run === null
|
|
126
|
+
|| latest.providerBinding.run.attemptId !== retry.currentAttemptId
|
|
127
|
+
|| latest.providerBinding.run.status !== "delivery-unknown")
|
|
128
|
+
return;
|
|
129
|
+
save({ ...latest, providerBinding: { ...latest.providerBinding,
|
|
130
|
+
retry: { ...latest.providerBinding.retry, reason: detail } } });
|
|
131
|
+
if (owner.scope === "task") {
|
|
132
|
+
const event = createTaskEvent(store.nextEventId(owner.taskId), owner.taskId, "provider.retry-unconfirmed", {
|
|
133
|
+
roleName: owner.roleName, attemptId: retry.currentAttemptId, nativeSessionId: retry.nativeSessionId, detail
|
|
134
|
+
}, new Date(at));
|
|
135
|
+
store.saveEvent(owner.taskId, event);
|
|
136
|
+
enqueueWork(store, owner.roleName === "leader" ? { kind: "operator" }
|
|
137
|
+
: { kind: "role", taskId: owner.taskId, roleName: "leader" }, "provider-retry-needs-attention", new Date(at), [{ type: "event", taskId: owner.taskId, id: event.id }]);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (retry.successorAutomatic !== false && at >= Date.parse(retry.deadline)) {
|
|
144
|
+
stop(set, "deadline-exhausted", at, true);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (at < Date.parse(retry.nextEligibleAt) || blocker !== undefined || binding.goal?.status === "active")
|
|
148
|
+
continue;
|
|
149
|
+
if (binding.run?.attemptId !== retry.failedAttemptId) {
|
|
150
|
+
stop(set, "superseded-input", at);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
if (owner.scope === "task" && retry.previousRunId !== undefined) {
|
|
155
|
+
if (retry.successorRunId !== undefined || retry.successorReviewRoundId !== undefined)
|
|
156
|
+
continue;
|
|
157
|
+
const previous = store.getRun(owner.taskId, retry.previousRunId);
|
|
158
|
+
if (previous?.status === "active")
|
|
159
|
+
continue; // ordinary rejected-delivery terminal fold
|
|
160
|
+
if (previous?.status !== "failed") {
|
|
161
|
+
stop(set, "run-no-longer-failed", at);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const actualTaskReviewCandidate = await ports.snapshotTaskCandidate?.(previous);
|
|
165
|
+
store.transaction(() => {
|
|
166
|
+
const latest = load(owner)?.providerBinding?.retry;
|
|
167
|
+
if (latest?.chainId !== retry.chainId || latest.failedAttemptId !== retry.failedAttemptId
|
|
168
|
+
|| latest.status !== "waiting")
|
|
169
|
+
return;
|
|
170
|
+
const reason = retryIntentBlocker(store, scoped, latest);
|
|
171
|
+
if (reason !== undefined)
|
|
172
|
+
throw new Error(reason);
|
|
173
|
+
runTaskCommand(["run", "retry", `${owner.taskId}/${previous.id}`], store, {
|
|
174
|
+
now: () => new Date(at), environment: {}, providerRetryChainId: retry.chainId,
|
|
175
|
+
...(actualTaskReviewCandidate === undefined ? {} : { actualTaskReviewCandidate })
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const original = retry.input.kind === "text" ? retry.input.text
|
|
181
|
+
: retry.input.kind === "wake"
|
|
182
|
+
? `Yui Task notification: task=${retry.input.taskId} wake=${retry.input.wakeId}.\nRead current Task context and that fixed wake window, including original Messages in full. This is not an execution assignment.`
|
|
183
|
+
: retry.input.kind === "message"
|
|
184
|
+
? `Yui Global Message: role=${retry.input.roleName} message=${retry.input.messageId}.\nRead your Session Context and referenced original Message in full.`
|
|
185
|
+
: undefined;
|
|
186
|
+
if (original === undefined) {
|
|
187
|
+
stop(set, "original-input-unavailable", at);
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const result = await (ports.submit ?? sendAgentHostRunControl)({
|
|
191
|
+
home, ...scoped, scope: owner.scope,
|
|
192
|
+
control: {
|
|
193
|
+
protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn", nativeSessionId: retry.nativeSessionId,
|
|
194
|
+
authority: { epoch: binding.authority.epoch, owner: "controller", holderId: binding.authority.holderId },
|
|
195
|
+
run: { attemptId: providerRetryAttemptId(retry), boundedText: original }
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
// Durable settlement belongs to Host/Controller ingress. In
|
|
199
|
+
// particular a lost response is never a reason to replay.
|
|
200
|
+
if (result.outcome === "rejected" && result.failure?.registrationDisposition === "not-committed"
|
|
201
|
+
&& result.failure.errorName !== "ProviderTurnBusyError") {
|
|
202
|
+
stop(set, result.failure.detail, at);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
// A send failure may have applied. A durable submitting/unknown
|
|
208
|
+
// identity fences it; do not manufacture a successor to uncertainty.
|
|
209
|
+
const latest = load(owner);
|
|
210
|
+
if (latest?.providerBinding?.run?.attemptId === retry.failedAttemptId) {
|
|
211
|
+
stop(set, error instanceof Error ? error.message : String(error), at);
|
|
212
|
+
}
|
|
213
|
+
ports.onError?.(error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -46,6 +46,8 @@ import { classifyRuntimeProcessExit, validateRuntimeProcessExitObservation } fro
|
|
|
46
46
|
import { replayRuntimeProcessExitOutbox } from "../runtime/processExitOutbox.js";
|
|
47
47
|
import { appendGlobalProcessExitObservation } from "../runtime/globalProcessExitStore.js";
|
|
48
48
|
import { deliverGlobalInputs } from "./globalInputDelivery.js";
|
|
49
|
+
import { providerRetryPrompt } from "../runtime/providerRetry.js";
|
|
50
|
+
import { createProviderRetryHooks } from "./providerRetryDelivery.js";
|
|
49
51
|
import { createGlobalRoleMessage } from "../message/message.js";
|
|
50
52
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
51
53
|
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
@@ -470,6 +472,26 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
470
472
|
now: options.now,
|
|
471
473
|
onError: options.onError,
|
|
472
474
|
lifecycleHost,
|
|
475
|
+
providerRetry: createProviderRetryHooks(home, store, {
|
|
476
|
+
now: options.now,
|
|
477
|
+
onError: options.onError,
|
|
478
|
+
snapshotTaskCandidate: async (run) => {
|
|
479
|
+
if (run.purpose !== "review" || run.reviewRoundId === undefined)
|
|
480
|
+
return undefined;
|
|
481
|
+
const round = store.getReviewRound(run.taskId, run.reviewRoundId);
|
|
482
|
+
if (round?.scope !== "task")
|
|
483
|
+
return undefined;
|
|
484
|
+
const task = store.getTask(run.taskId);
|
|
485
|
+
const workspace = store.getTaskWorkspace(run.taskId);
|
|
486
|
+
if (task === null || workspace === null || !(workspacePreparer instanceof FileTaskWorkspacePreparer)) {
|
|
487
|
+
throw new Error("Task-final retry requires an observable authoritative Task workspace.");
|
|
488
|
+
}
|
|
489
|
+
const snapshot = await workspacePreparer.snapshotDirectTaskMain(workspace, task.projectBindings.map(p => p.projectId));
|
|
490
|
+
return { schemaVersion: 1, projects: snapshot.projects.map(p => ({
|
|
491
|
+
projectId: p.projectId, commit: p.headCommit
|
|
492
|
+
})) };
|
|
493
|
+
}
|
|
494
|
+
}),
|
|
473
495
|
jobSupervisor,
|
|
474
496
|
globalInputDelivery: () => deliverGlobalInputs(home, store, async (roleName) => {
|
|
475
497
|
await lifecycleDispatcher("runtime.ensure-role-session", { scope: "global", roleName });
|
|
@@ -617,6 +639,8 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
617
639
|
authorityEpoch: value.authorityEpoch,
|
|
618
640
|
authorityOwner: value.authorityOwner,
|
|
619
641
|
holderId: value.holderId,
|
|
642
|
+
boundedText: value.boundedText,
|
|
643
|
+
retrySupported: value.retrySupported,
|
|
620
644
|
now: value.now
|
|
621
645
|
});
|
|
622
646
|
}
|
|
@@ -628,7 +652,15 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
628
652
|
}
|
|
629
653
|
throw error;
|
|
630
654
|
}
|
|
631
|
-
|
|
655
|
+
const binding = (value.taskId === undefined
|
|
656
|
+
? store.getGlobalRoleSessionSet(value.roleName)
|
|
657
|
+
: store.getTaskRoleSessionSet(value.taskId, value.roleName))?.providerBinding;
|
|
658
|
+
const retry = binding?.retry;
|
|
659
|
+
return { recorded: true, ...(retry?.currentAttemptId === value.attemptId && value.boundedText !== undefined
|
|
660
|
+
? { retryInput: {
|
|
661
|
+
text: providerRetryPrompt(retry, value.boundedText),
|
|
662
|
+
...(retry.failedNativeTurnId === undefined ? {} : { expectedFailedNativeTurnId: retry.failedNativeTurnId })
|
|
663
|
+
} } : {}) };
|
|
632
664
|
}
|
|
633
665
|
if (method === "runtime.provider-turn-submission-resolve") {
|
|
634
666
|
const value = providerTurnControlParams(params);
|
|
@@ -648,6 +680,7 @@ export function createRuntimeLifecycleDispatcher(store, schedulerStore, sessionH
|
|
|
648
680
|
status,
|
|
649
681
|
reason,
|
|
650
682
|
raw,
|
|
683
|
+
noProviderWrite: params.providerWrite === "not-issued",
|
|
651
684
|
now: value.now
|
|
652
685
|
});
|
|
653
686
|
return { recorded: true };
|
|
@@ -1207,6 +1240,8 @@ function providerTurnControlParams(params) {
|
|
|
1207
1240
|
authorityEpoch: authorityEpoch,
|
|
1208
1241
|
authorityOwner,
|
|
1209
1242
|
holderId: requiredParam(value.holderId),
|
|
1243
|
+
...(value.boundedText === undefined ? {} : { boundedText: requiredParam(value.boundedText) }),
|
|
1244
|
+
...(value.retrySupport === undefined ? {} : { retrySupported: value.retrySupport === "codex-failed-turn-v1" }),
|
|
1210
1245
|
now: new Date(observedAt)
|
|
1211
1246
|
};
|
|
1212
1247
|
}
|
|
@@ -76,7 +76,7 @@ export class GitIntegrationService {
|
|
|
76
76
|
#resourceRegistrar() {
|
|
77
77
|
return this.#resourceRegistrarValue ??= new ResourceRegistrar(this.home, this.now);
|
|
78
78
|
}
|
|
79
|
-
async integrate(taskId, integrationId) {
|
|
79
|
+
async integrate(taskId, integrationId, signal) {
|
|
80
80
|
let initial = requireIntegration(this.store, taskId, integrationId);
|
|
81
81
|
if (!["running", "conflicted", "blocked", "validating"].includes(initial.status)) {
|
|
82
82
|
throw new Error(`Integration cannot continue from ${initial.status}.`);
|
|
@@ -86,7 +86,7 @@ export class GitIntegrationService {
|
|
|
86
86
|
// CAS. A concurrent `project migrate` must not switch the catalog path or
|
|
87
87
|
// remove the old checkout mid-Integration, so the per-Project maintenance
|
|
88
88
|
// fence is held across every Git effect and released only on exit.
|
|
89
|
-
const release = acquireProjectMaintenanceLocks(this.home, [initial.projectId]);
|
|
89
|
+
const release = await acquireProjectMaintenanceLocks(this.home, [initial.projectId], { signal });
|
|
90
90
|
try {
|
|
91
91
|
// A concurrent abort may have settled the attempt while this caller was
|
|
92
92
|
// acquiring the same Project fence. Never execute a pre-fence snapshot.
|
|
@@ -260,9 +260,9 @@ export class GitIntegrationService {
|
|
|
260
260
|
/** Abandon under the same fence as Git/CAS, not just a status whitelist.
|
|
261
261
|
* A delivered validating candidate is recorded as committed; a provably
|
|
262
262
|
* unadvanced one may be abandoned without requiring obsolete check settings. */
|
|
263
|
-
async abort(taskId, integrationId, reason, authorize) {
|
|
263
|
+
async abort(taskId, integrationId, reason, authorize, signal) {
|
|
264
264
|
const initial = requireIntegration(this.store, taskId, integrationId);
|
|
265
|
-
const release = acquireProjectMaintenanceLocks(this.home, [initial.projectId]);
|
|
265
|
+
const release = await acquireProjectMaintenanceLocks(this.home, [initial.projectId], { signal });
|
|
266
266
|
try {
|
|
267
267
|
const attempt = requireIntegration(this.store, taskId, integrationId);
|
|
268
268
|
authorize(this.store, taskId);
|
|
@@ -338,12 +338,25 @@ export class GitIntegrationService {
|
|
|
338
338
|
release();
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
|
-
async cleanup(integration) {
|
|
341
|
+
async cleanup(integration, options = {}) {
|
|
342
342
|
// The whole cleanup is one Git transaction against the Project's
|
|
343
343
|
// repository, mirroring integrate(): a concurrent `project migrate` must
|
|
344
344
|
// not switch the catalog path or remove the old checkout mid-cleanup.
|
|
345
|
-
const release = acquireProjectMaintenanceLocks(this.home, [integration.projectId]);
|
|
345
|
+
const release = await acquireProjectMaintenanceLocks(this.home, [integration.projectId], { signal: options.signal });
|
|
346
346
|
try {
|
|
347
|
+
options.authorize?.();
|
|
348
|
+
const current = requireIntegration(this.store, integration.taskId, integration.id);
|
|
349
|
+
if (current.projectId !== integration.projectId
|
|
350
|
+
|| !["committed", "superseded", "failed"].includes(current.status)) {
|
|
351
|
+
throw new Error("Integration changed before cleanup; read its current record.");
|
|
352
|
+
}
|
|
353
|
+
integration = current;
|
|
354
|
+
const activeJob = this.store.listDurableJobs(integration.taskId).find(job => job.owner.kind === "integration-attempt" && job.owner.integrationAttemptId === integration.id
|
|
355
|
+
&& (job.status === "queued" || job.status === "running"
|
|
356
|
+
|| (job.status === "unknown-needs-attention" && job.acknowledgedAt === undefined)));
|
|
357
|
+
if (activeJob !== undefined) {
|
|
358
|
+
throw new Error(`Integration ${integration.id} has an active DurableJob: ${activeJob.id}/${activeJob.status}.`);
|
|
359
|
+
}
|
|
347
360
|
const task = this.store.getTask(integration.taskId);
|
|
348
361
|
if (task === null || !task.projectBindings.some(({ projectId }) => projectId === integration.projectId)) {
|
|
349
362
|
throw new Error(`Integration Task Project is unavailable: ${integration.id}.`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { createTaskMessage } from "../message/message.js";
|
|
3
|
+
import { providerRetryPending } from "../runtime/providerRetry.js";
|
|
3
4
|
import { captureRoleRunDispatch, enqueueWork, settleRoleRunDispatch } from "../coordination/workMailboxQueue.js";
|
|
4
5
|
import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewRound.js";
|
|
5
6
|
import { completeRun, failRun, runPurposeAdmitsTaskState } from "../agentRun/agentRun.js";
|
|
@@ -394,7 +395,9 @@ export function terminalizeExactTaskRun(store, input, now) {
|
|
|
394
395
|
const message = createTaskMessage(store.nextMessageId(terminal.taskId), terminal.taskId, `Execution ${terminal.id} ${terminal.status}.`, "role-result", { type: "role", roleName: terminal.roleName }, now, { resultRef: { type: "agent-run-result", runId: terminal.id }, ...(terminal.workItemId === undefined
|
|
395
396
|
? {} : { workItemId: terminal.workItemId }) });
|
|
396
397
|
store.saveMessage(terminal.taskId, message);
|
|
397
|
-
|
|
398
|
+
if (!providerRetryPending(store.getTaskRoleSessionSet(terminal.taskId, terminal.roleName)?.providerBinding)) {
|
|
399
|
+
enqueueWork(store, { kind: "role", taskId: terminal.taskId, roleName: "leader" }, "role-result", now, [{ type: "message", taskId: terminal.taskId, id: message.id }], { source: "task-event", dedupeKey: `result:${terminal.taskId}/${terminal.id}` });
|
|
400
|
+
}
|
|
398
401
|
}
|
|
399
402
|
const dispatchIdentity = {
|
|
400
403
|
taskId: terminal.taskId,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { markGlobalRoleMessageNotDelivered } from "./message.js";
|
|
2
|
+
/** Settle the original queue entry when recovery ends with exact nondelivery.
|
|
3
|
+
* Cancellation of a submitted/unknown request is not negative Provider proof. */
|
|
4
|
+
export function settleGlobalRetryInput(store, roleName, binding, now) {
|
|
5
|
+
const retry = binding?.retry;
|
|
6
|
+
if (retry === undefined || retry.input.kind !== "message" || retry.input.roleName !== roleName
|
|
7
|
+
|| !["cancelled", "exhausted", "needs-attention"].includes(retry.status)
|
|
8
|
+
|| binding?.run == null || ["submitting", "accepted", "delivery-unknown"].includes(binding.run.status))
|
|
9
|
+
return;
|
|
10
|
+
const messageId = retry.input.messageId;
|
|
11
|
+
const message = store.listGlobalRoleMessages(roleName).find(m => m.id === messageId);
|
|
12
|
+
if (message === undefined || message.delivery !== undefined || message.notDelivered !== undefined)
|
|
13
|
+
return;
|
|
14
|
+
store.updateGlobalRoleMessage(markGlobalRoleMessageNotDelivered(message, `provider-retry-${retry.status}: ${retry.reason ?? retry.failureRef}`, now));
|
|
15
|
+
}
|
|
@@ -15,6 +15,7 @@ import { join } from "node:path";
|
|
|
15
15
|
import { openCurrentTaskStore } from "../storage/currentTaskStore.js";
|
|
16
16
|
import { resolveTaskStoreBackendForHome } from "../storage/sqliteStore.js";
|
|
17
17
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
18
|
+
import { projectTaskUsageMetrics } from "../runtime/taskUsageMetrics.js";
|
|
18
19
|
import { classifyRunFailure, classifyIntegrationAttempt, classifyReviewRound, countFaultClasses } from "./faultClassification.js";
|
|
19
20
|
import { RUNTIME_LAUNCH_KINDS, RUNTIME_LAUNCH_PHASES } from "../runtime/launchDiagnostics.js";
|
|
20
21
|
import { UNSUPPORTED } from "./runtimeIdentity.js";
|
|
@@ -192,6 +193,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
192
193
|
agentErrors: section,
|
|
193
194
|
workItems: section,
|
|
194
195
|
orchestration: section,
|
|
196
|
+
usage: section,
|
|
195
197
|
storage: section,
|
|
196
198
|
runtimeProtocol: section,
|
|
197
199
|
topLongRunning: section
|
|
@@ -658,6 +660,22 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
658
660
|
return failed(error);
|
|
659
661
|
}
|
|
660
662
|
})();
|
|
663
|
+
const usage = (() => {
|
|
664
|
+
try {
|
|
665
|
+
return ok(taskIds.map((taskId) => {
|
|
666
|
+
const task = store.getTask(taskId);
|
|
667
|
+
if (task === null)
|
|
668
|
+
throw new Error(`Task not found: ${taskId}.`);
|
|
669
|
+
return projectTaskUsageMetrics({
|
|
670
|
+
task, events: store.listEvents(taskId), runs: store.listRuns(taskId),
|
|
671
|
+
now: new Date(generatedAt)
|
|
672
|
+
});
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
return failed(error);
|
|
677
|
+
}
|
|
678
|
+
})();
|
|
661
679
|
const storage = (() => {
|
|
662
680
|
try {
|
|
663
681
|
let stateJsonBytes = UNSUPPORTED;
|
|
@@ -796,6 +814,7 @@ export function runExecutionAudit(home, options = {}, ports = createProductionEx
|
|
|
796
814
|
agentErrors,
|
|
797
815
|
workItems,
|
|
798
816
|
orchestration,
|
|
817
|
+
usage,
|
|
799
818
|
storage,
|
|
800
819
|
runtimeProtocol,
|
|
801
820
|
topLongRunning
|